From 72dbca0d35f01f1ff5a7edff2aab90cff9bdf83c Mon Sep 17 00:00:00 2001 From: geweald Date: Thu, 2 Jun 2022 12:38:52 +0200 Subject: [PATCH 01/49] feat(persistence): allow experimental indexedDB on web --- packages/stream_chat_persistence/CHANGELOG.md | 1 + .../lib/src/db/shared/native_db.dart | 5 ++-- .../lib/src/db/shared/unsupported_db.dart | 3 ++- .../lib/src/db/shared/web_db.dart | 25 ++++++++++++++++--- .../src/stream_chat_persistence_client.dart | 18 ++++++++++--- 5 files changed, 42 insertions(+), 10 deletions(-) diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index b3d0e686..a2b195f2 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,6 +1,7 @@ ## Upcoming - Added support for `Channel.ownCapabilities` +- Allowed experimental use of indexedDb on web with `webUseExperimentalIndexedDb` parameter on `StreamChatPersistenceClient` ## 4.1.0 diff --git a/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart b/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart index a30dd59d..ca9a98bf 100644 --- a/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart +++ b/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart @@ -15,11 +15,12 @@ import 'package:stream_chat_persistence/stream_chat_persistence.dart'; /// specifically for native platform applications. class SharedDB { /// Returns a new instance of [DriftChatDatabase]. - static DriftChatDatabase constructDatabase( + static Future constructDatabase( String userId, { bool logStatements = false, ConnectionMode connectionMode = ConnectionMode.regular, - }) { + bool webUseIndexedDbIfSupported = false, // Ignored on native + }) async { final dbName = 'db_$userId'; if (connectionMode == ConnectionMode.background) { return DriftChatDatabase.connect( diff --git a/packages/stream_chat_persistence/lib/src/db/shared/unsupported_db.dart b/packages/stream_chat_persistence/lib/src/db/shared/unsupported_db.dart index c840aa5f..b92105c5 100644 --- a/packages/stream_chat_persistence/lib/src/db/shared/unsupported_db.dart +++ b/packages/stream_chat_persistence/lib/src/db/shared/unsupported_db.dart @@ -5,10 +5,11 @@ import 'package:stream_chat_persistence/stream_chat_persistence.dart'; /// A Helper class to construct new instances of [DriftChatDatabase] class SharedDB { /// Returns a new instance of [DriftChatDatabase]. - static DriftChatDatabase constructDatabase( + static Future constructDatabase( String userId, { bool logStatements = false, ConnectionMode connectionMode = ConnectionMode.regular, + bool webUseIndexedDbIfSupported = false, }) { throw UnsupportedError( 'No implementation of the constructDatabase api provided', diff --git a/packages/stream_chat_persistence/lib/src/db/shared/web_db.dart b/packages/stream_chat_persistence/lib/src/db/shared/web_db.dart index 394b38d0..89dc864b 100644 --- a/packages/stream_chat_persistence/lib/src/db/shared/web_db.dart +++ b/packages/stream_chat_persistence/lib/src/db/shared/web_db.dart @@ -7,13 +7,32 @@ import 'package:stream_chat_persistence/src/stream_chat_persistence_client.dart' /// specifically for Web applications. class SharedDB { /// Returns a new instance of [DriftChatDatabase]. - static DriftChatDatabase constructDatabase( + static Future constructDatabase( String userId, { bool logStatements = false, ConnectionMode connectionMode = ConnectionMode.regular, // Ignored on web - }) { + bool webUseIndexedDbIfSupported = false, + }) async { final dbName = 'db_$userId'; - final queryExecutor = WebDatabase(dbName, logStatements: logStatements); + final queryExecutor = await _getQueryExecutor( + dbName, + useIndexedDbIfSupported: webUseIndexedDbIfSupported, + logStatements: logStatements, + ); return DriftChatDatabase(userId, queryExecutor); } + + static Future _getQueryExecutor( + String dbName, { + required bool useIndexedDbIfSupported, + required bool logStatements, + }) async { + if (useIndexedDbIfSupported) { + return WebDatabase.withStorage( + await DriftWebStorage.indexedDbIfSupported(dbName), + logStatements: logStatements, + ); + } + return WebDatabase(dbName, logStatements: logStatements); + } } diff --git a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart index 6eb29139..28b93588 100644 --- a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart +++ b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart @@ -1,7 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:mutex/mutex.dart'; import 'package:stream_chat/stream_chat.dart'; - import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; /// Various connection modes on which [StreamChatPersistenceClient] can work @@ -29,8 +28,14 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { /// Connection mode on which the client will work ConnectionMode connectionMode = ConnectionMode.regular, Level logLevel = Level.WARNING, + + /// Whether to use an experimental storage implementation on the web + /// that uses IndexedDB if the current browser supports it. + /// Otherwise, falls back to the local storage based implementation. + bool webUseExperimentalIndexedDb = false, LogHandlerFunction? logHandlerFunction, }) : _connectionMode = connectionMode, + _webUseIndexedDbIfSupported = webUseExperimentalIndexedDb, _logger = Logger.detached('💽')..level = logLevel { _logger.onRecord.listen(logHandlerFunction ?? _defaultLogHandler); } @@ -41,6 +46,7 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { final Logger _logger; final ConnectionMode _connectionMode; + final bool _webUseIndexedDbIfSupported; final _mutex = ReadWriteMutex(); void _defaultLogHandler(LogRecord record) { @@ -68,11 +74,15 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { return true; } - DriftChatDatabase _defaultDatabaseProvider( + Future _defaultDatabaseProvider( String userId, ConnectionMode mode, ) => - SharedDB.constructDatabase(userId, connectionMode: mode); + SharedDB.constructDatabase( + userId, + connectionMode: mode, + webUseIndexedDbIfSupported: _webUseIndexedDbIfSupported, + ); @override Future connect( @@ -86,7 +96,7 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { ); } db = databaseProvider?.call(userId, _connectionMode) ?? - _defaultDatabaseProvider(userId, _connectionMode); + await _defaultDatabaseProvider(userId, _connectionMode); } @override From e5f77076a945f081f3793bbe26376e131561bc36 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 23 Jun 2022 15:35:07 +0200 Subject: [PATCH 02/49] fix(doc): push notification guide now uses the latest cli version --- docusaurus/docs/Flutter/guides/adding_push_notifications.mdx | 2 +- docusaurus/docs/Flutter/guides/adding_push_notifications_v2.mdx | 2 +- .../version-3.x.x/Flutter/guides/adding_push_notifications.mdx | 2 +- .../Flutter/guides/adding_push_notifications_v2.mdx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docusaurus/docs/Flutter/guides/adding_push_notifications.mdx b/docusaurus/docs/Flutter/guides/adding_push_notifications.mdx index 328ba546..e845c3c6 100644 --- a/docusaurus/docs/Flutter/guides/adding_push_notifications.mdx +++ b/docusaurus/docs/Flutter/guides/adding_push_notifications.mdx @@ -128,7 +128,7 @@ If you're not sure if you've set up push notifications correctly (e.g. you don't 11. After configuring [stream-cli](https://github.com/GetStream/stream-cli) paste the following command on command line using your user ID ```shell -stream chat:push:test -u +stream-cli chat test-push --user-id ``` You should get a test push notification diff --git a/docusaurus/docs/Flutter/guides/adding_push_notifications_v2.mdx b/docusaurus/docs/Flutter/guides/adding_push_notifications_v2.mdx index 917d9216..a03efe9a 100644 --- a/docusaurus/docs/Flutter/guides/adding_push_notifications_v2.mdx +++ b/docusaurus/docs/Flutter/guides/adding_push_notifications_v2.mdx @@ -223,7 +223,7 @@ If you're not sure whether you've set up push notifications correctly, for examp 9. Tap on `Device ID` and copy it 11. After configuring [stream-cli](https://github.com/GetStream/stream-cli), run the following command using your user ID: ```shell -stream chat:push:test -u +stream-cli chat test-push --user-id ``` You should get a test push notification 🥳 diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_push_notifications.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_push_notifications.mdx index 328ba546..e845c3c6 100644 --- a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_push_notifications.mdx +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_push_notifications.mdx @@ -128,7 +128,7 @@ If you're not sure if you've set up push notifications correctly (e.g. you don't 11. After configuring [stream-cli](https://github.com/GetStream/stream-cli) paste the following command on command line using your user ID ```shell -stream chat:push:test -u +stream-cli chat test-push --user-id ``` You should get a test push notification diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_push_notifications_v2.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_push_notifications_v2.mdx index 285c2205..dc9f5a32 100644 --- a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_push_notifications_v2.mdx +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_push_notifications_v2.mdx @@ -215,7 +215,7 @@ If you're not sure whether you've set up push notifications correctly, for examp 9. Tap on `Device ID` and copy it 11. After configuring [stream-cli](https://github.com/GetStream/stream-cli), run the following command using your user ID: ```shell -stream chat:push:test -u +stream-cli chat test-push --user-id ``` You should get a test push notification 🥳 From 05d376cffb39f94a5420fdb0b5bd986911d08680 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 4 Jul 2022 11:58:52 +0200 Subject: [PATCH 03/49] fix(core): allow scroll notification bubbling in lazy load scroll view --- .../lib/src/lazy_load_scroll_view.dart | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/stream_chat_flutter_core/lib/src/lazy_load_scroll_view.dart b/packages/stream_chat_flutter_core/lib/src/lazy_load_scroll_view.dart index e086ab55..5c6ad9c0 100644 --- a/packages/stream_chat_flutter_core/lib/src/lazy_load_scroll_view.dart +++ b/packages/stream_chat_flutter_core/lib/src/lazy_load_scroll_view.dart @@ -17,6 +17,7 @@ class LazyLoadScrollView extends StatefulWidget { this.onPageScrollEnd, this.onInBetweenOfPage, this.scrollOffset = 100, + this.allowNotificationBubbling = false, }); /// The [Widget] that this widget watches for changes on @@ -40,6 +41,9 @@ class LazyLoadScrollView extends StatefulWidget { /// The offset to take into account when triggering [onEndOfPage]/[onStartOfPage] in pixels final double scrollOffset; + /// If true the notifications will keep bubbling up the tree + final bool allowNotificationBubbling; + @override State createState() => _LazyLoadScrollViewState(); } @@ -59,13 +63,13 @@ class _LazyLoadScrollViewState extends State { if (notification is ScrollStartNotification) { if (widget.onPageScrollStart != null) { widget.onPageScrollStart!(); - return true; + return widget.allowNotificationBubbling; } } if (notification is ScrollEndNotification) { if (widget.onPageScrollEnd != null) { widget.onPageScrollEnd!(); - return true; + return widget.allowNotificationBubbling; } } if (notification is ScrollUpdateNotification) { @@ -78,7 +82,7 @@ class _LazyLoadScrollViewState extends State { pixels < (maxScrollExtent - scrollOffset)) { if (widget.onInBetweenOfPage != null) { widget.onInBetweenOfPage!(); - return true; + return widget.allowNotificationBubbling; } } @@ -90,12 +94,12 @@ class _LazyLoadScrollViewState extends State { if (scrollingDown) { if (extentAfter <= scrollOffset) { _onEndOfPage(); - return true; + return widget.allowNotificationBubbling; } } else { if (extentBefore <= scrollOffset) { _onStartOfPage(); - return true; + return widget.allowNotificationBubbling; } } } @@ -106,9 +110,9 @@ class _LazyLoadScrollViewState extends State { if (notification.overscroll < 0) { _onStartOfPage(); } - return true; + return widget.allowNotificationBubbling; } - return false; + return widget.allowNotificationBubbling; } void _onEndOfPage() { From 816746da8a1a579f0c3d85d13fee2c87ea1e83ae Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 4 Jul 2022 16:41:50 +0530 Subject: [PATCH 04/49] fix(ui): Fix `ChannelListTile` sendingIndicator `isMessageRead` calculation. Signed-off-by: xsahil03x --- packages/stream_chat_flutter/CHANGELOG.md | 10 +++++++++- .../stream_channel_list_tile.dart | 14 +++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index adf61a98..10b85eb3 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,10 @@ +## Upcoming + +🐞 Fixed + +- [[#1234]](https://github.com/GetStream/stream-chat-flutter/issues/1234) Fix `ChannelListTile` + sendingIndicator `isMessageRead` calculation. + ## 4.3.0 - Updated `photo_view` dependency to [`0.14.0`](https://pub.dev/packages/photo_view/changelog). @@ -9,7 +16,8 @@ - [[#996]](https://github.com/GetStream/stream-chat-flutter/issues/996) Videos break bottom photo carousal. - Fix: URLs with path and/or query params are not enriched. -- [[#1194]](https://github.com/GetStream/stream-chat-flutter/issues/1194) Request permission to access gallery when opening the file picker. +- [[#1194]](https://github.com/GetStream/stream-chat-flutter/issues/1194) Request permission to + access gallery when opening the file picker. ✅ Added diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_list_tile.dart b/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_list_tile.dart index 129875cf..aa6511d8 100644 --- a/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_list_tile.dart @@ -200,6 +200,14 @@ class StreamChannelListTile extends StatelessWidget { return const Offstage(); } + final memberReadCount = channelState.read.where((it) { + return it.user.id != currentUser.id && + (it.lastRead.isAfter(lastMessage.createdAt) || + it.lastRead.isAtSameMomentAs( + lastMessage.createdAt, + )); + }).length; + return Padding( padding: const EdgeInsets.only(right: 4), child: @@ -207,9 +215,9 @@ class StreamChannelListTile extends StatelessWidget { StreamSendingIndicator( message: lastMessage, size: channelPreviewTheme.indicatorIconSize, - isMessageRead: channelState - .currentUserRead!.lastRead - .isAfter(lastMessage.createdAt), + isMessageRead: memberReadCount >= + // Subtract one for the current user. + (channel.memberCount ?? 0) - 1, ), ); }, From d2883c87f08711af11e086523b0d9bdbd97bff6f Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 4 Jul 2022 15:32:31 +0200 Subject: [PATCH 05/49] fix(ui): fix lazy load scroll view --- .../lib/src/lazy_load_scroll_view.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/stream_chat_flutter_core/lib/src/lazy_load_scroll_view.dart b/packages/stream_chat_flutter_core/lib/src/lazy_load_scroll_view.dart index 5c6ad9c0..d56d5f23 100644 --- a/packages/stream_chat_flutter_core/lib/src/lazy_load_scroll_view.dart +++ b/packages/stream_chat_flutter_core/lib/src/lazy_load_scroll_view.dart @@ -63,13 +63,13 @@ class _LazyLoadScrollViewState extends State { if (notification is ScrollStartNotification) { if (widget.onPageScrollStart != null) { widget.onPageScrollStart!(); - return widget.allowNotificationBubbling; + return !widget.allowNotificationBubbling; } } if (notification is ScrollEndNotification) { if (widget.onPageScrollEnd != null) { widget.onPageScrollEnd!(); - return widget.allowNotificationBubbling; + return !widget.allowNotificationBubbling; } } if (notification is ScrollUpdateNotification) { @@ -82,7 +82,7 @@ class _LazyLoadScrollViewState extends State { pixels < (maxScrollExtent - scrollOffset)) { if (widget.onInBetweenOfPage != null) { widget.onInBetweenOfPage!(); - return widget.allowNotificationBubbling; + return !widget.allowNotificationBubbling; } } @@ -94,12 +94,12 @@ class _LazyLoadScrollViewState extends State { if (scrollingDown) { if (extentAfter <= scrollOffset) { _onEndOfPage(); - return widget.allowNotificationBubbling; + return !widget.allowNotificationBubbling; } } else { if (extentBefore <= scrollOffset) { _onStartOfPage(); - return widget.allowNotificationBubbling; + return !widget.allowNotificationBubbling; } } } @@ -110,9 +110,9 @@ class _LazyLoadScrollViewState extends State { if (notification.overscroll < 0) { _onStartOfPage(); } - return widget.allowNotificationBubbling; + return !widget.allowNotificationBubbling; } - return widget.allowNotificationBubbling; + return false; } void _onEndOfPage() { From a37bf2bde42888d663b4e0e0bf08ed0d14cc5a40 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 5 Jul 2022 12:08:08 +0200 Subject: [PATCH 06/49] fix(llc): fix connecting while connecting and disconneting (#1237) * fix(llc): fix connecting while connecting and disconneting * chore(llc): update changelog * chore(llc): add test * chore(llc): improve test * fix(llc): _resetRequestFlags regardless of the connection status --- packages/stream_chat/CHANGELOG.md | 6 ++++ .../stream_chat/lib/src/ws/websocket.dart | 4 +++ .../test/src/ws/websocket_test.dart | 34 +++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 2d17f32e..d336e017 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +🐞 Fixed + +- Fix WebSocket contemporary connection calls while disconnecting + ## 4.3.0 🐞 Fixed diff --git a/packages/stream_chat/lib/src/ws/websocket.dart b/packages/stream_chat/lib/src/ws/websocket.dart index 2287481e..a86ba6c5 100644 --- a/packages/stream_chat/lib/src/ws/websocket.dart +++ b/packages/stream_chat/lib/src/ws/websocket.dart @@ -436,6 +436,9 @@ class WebSocket with TimerHelper { /// Disconnects the WS and releases eventual resources void disconnect() { if (connectionStatus == ConnectionStatus.disconnected) return; + + _resetRequestFlags(resetAttempts: true); + _connectionStatus = ConnectionStatus.disconnected; _logger?.info('Disconnecting web-socket connection'); @@ -447,6 +450,7 @@ class WebSocket with TimerHelper { _stopMonitoringEvents(); _manuallyClosed = true; + _closeWebSocketChannel(); } } diff --git a/packages/stream_chat/test/src/ws/websocket_test.dart b/packages/stream_chat/test/src/ws/websocket_test.dart index 68242333..9489bc78 100644 --- a/packages/stream_chat/test/src/ws/websocket_test.dart +++ b/packages/stream_chat/test/src/ws/websocket_test.dart @@ -131,6 +131,40 @@ void main() { addTearDown(timer.cancel); }); + test('`connect`, `disconnect` and `connect` again without waiting', () async { + final user = OwnUser( + id: 'test-user', + name: 'test', + ); + const connectionId = 'test-connection-id'; + // Sends connect event to web-socket stream + final timer = Timer.periodic(const Duration(milliseconds: 300), (_) { + final event = Event( + type: EventType.healthCheck, + connectionId: connectionId, + me: user, + ); + webSocketSink.add(json.encode(event)); + }); + + await webSocket.connect( + user, + ); + + webSocket + ..disconnect() + ..connect(user) + ..disconnect(); + final event = await webSocket.connect(user); + + expect(event.type, EventType.healthCheck); + expect(event.connectionId, connectionId); + expect(event.me, isNotNull); + expect(event.me!.id, user.id); + + addTearDown(timer.cancel); + }); + test('`connect` should throw if already in connection attempt', () async { final user = OwnUser(id: 'test-user'); webSocket.connect(user); From dd47db04dde5ed888a9e8967bc6dc98e5d27cdae Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 6 Jul 2022 18:13:15 +0530 Subject: [PATCH 07/49] feat(llc): add support for AttachmentFileUploaderProvider. (#1246) * feat(llc): add support for `AttachmentFileUploaderProvider`. Signed-off-by: xsahil03x * chore(llc): update CHANGELOG.md Signed-off-by: xsahil03x --- packages/stream_chat/CHANGELOG.md | 9 ++++++++ .../stream_chat/lib/src/client/channel.dart | 1 - .../stream_chat/lib/src/client/client.dart | 23 ++++++++++++++++--- .../core/api/attachment_file_uploader.dart | 5 ++++ .../lib/src/core/api/stream_chat_api.dart | 8 ++++--- packages/stream_chat/lib/stream_chat.dart | 6 ++--- packages/stream_chat/test/src/fakes.dart | 1 - 7 files changed, 42 insertions(+), 11 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index d336e017..11e630fb 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -4,6 +4,15 @@ - Fix WebSocket contemporary connection calls while disconnecting +✅ Added + +- Export `StreamAttachmentFileUploader`. + +🔄 Changed + +- Deprecated `StreamChatClient.attachmentFileUploader`, + Use `StreamChatClient.attachmentFileUploaderProvider` instead. + ## 4.3.0 🐞 Fixed diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index b4e1379d..64c20d6b 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -3,7 +3,6 @@ import 'dart:math'; import 'package:collection/collection.dart' show IterableExtension, ListEquality; -import 'package:dio/dio.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/client/retry_queue.dart'; import 'package:stream_chat/src/core/util/utils.dart'; diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 0b31db9e..a8c4dd7b 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -70,8 +70,15 @@ class StreamChatClient { Duration receiveTimeout = const Duration(seconds: 6), StreamChatApi? chatApi, WebSocket? ws, - AttachmentFileUploader? attachmentFileUploader, - }) { + @Deprecated('Use [attachmentFileUploaderProvider] instead') + AttachmentFileUploader? attachmentFileUploader, + AttachmentFileUploaderProvider? attachmentFileUploaderProvider, + }) : assert( + attachmentFileUploader == null || + attachmentFileUploaderProvider == null, + 'You can only use one of [attachmentFileUploader] ' + 'or [attachmentFileUploaderProvider]', + ) { logger.info('Initiating new StreamChatClient'); final options = StreamHttpClientOptions( @@ -81,13 +88,23 @@ class StreamChatClient { headers: {'X-Stream-Client': defaultUserAgent}, ); + // TODO: simplify this once we remove the deprecated field. + final AttachmentFileUploaderProvider fileUploaderProvider; + if (attachmentFileUploaderProvider != null) { + fileUploaderProvider = attachmentFileUploaderProvider; + } else if (attachmentFileUploader != null) { + fileUploaderProvider = (httpClient) => attachmentFileUploader; + } else { + fileUploaderProvider = StreamAttachmentFileUploader.new; + } + _chatApi = chatApi ?? StreamChatApi( apiKey, options: options, tokenManager: _tokenManager, connectionIdManager: _connectionIdManager, - attachmentFileUploader: attachmentFileUploader, + attachmentFileUploaderProvider: fileUploaderProvider, logger: detachedLogger('🕸️'), ); diff --git a/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart b/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart index 096b2c2d..c851d114 100644 --- a/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart +++ b/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart @@ -3,6 +3,11 @@ import 'package:stream_chat/src/core/api/responses.dart'; import 'package:stream_chat/src/core/http/stream_http_client.dart'; import 'package:stream_chat/src/core/models/attachment_file.dart'; +/// Signature for a function which provides instance of [AttachmentFileUploader] +typedef AttachmentFileUploaderProvider = AttachmentFileUploader Function( + StreamHttpClient httpClient, +); + /// Class responsible for uploading images and files to a given channel abstract class AttachmentFileUploader { /// Uploads a [image] to the given channel. diff --git a/packages/stream_chat/lib/src/core/api/stream_chat_api.dart b/packages/stream_chat/lib/src/core/api/stream_chat_api.dart index bcf041c1..cf6c0bad 100644 --- a/packages/stream_chat/lib/src/core/api/stream_chat_api.dart +++ b/packages/stream_chat/lib/src/core/api/stream_chat_api.dart @@ -22,9 +22,10 @@ class StreamChatApi { StreamHttpClientOptions? options, TokenManager? tokenManager, ConnectionIdManager? connectionIdManager, - AttachmentFileUploader? attachmentFileUploader, + AttachmentFileUploaderProvider? attachmentFileUploaderProvider, Logger? logger, - }) : _fileUploader = attachmentFileUploader, + }) : _fileUploaderProvider = + attachmentFileUploaderProvider ?? StreamAttachmentFileUploader.new, _client = client ?? StreamHttpClient( apiKey, @@ -35,6 +36,7 @@ class StreamChatApi { ); final StreamHttpClient _client; + final AttachmentFileUploaderProvider _fileUploaderProvider; UserApi? _user; @@ -75,5 +77,5 @@ class StreamChatApi { /// Class responsible for uploading images and files to a given channel AttachmentFileUploader get fileUploader => - _fileUploader ??= StreamAttachmentFileUploader(_client); + _fileUploader ??= _fileUploaderProvider.call(_client); } diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 0479a378..27ba68cc 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -1,6 +1,7 @@ library stream_chat; export 'package:async/async.dart'; +export 'package:dio/src/cancel_token.dart'; export 'package:dio/src/dio_error.dart'; export 'package:dio/src/multipart_file.dart'; export 'package:dio/src/options.dart'; @@ -9,8 +10,7 @@ export 'package:logging/logging.dart' show Logger, Level, LogRecord; export 'package:rate_limiter/rate_limiter.dart'; export 'package:uuid/uuid.dart'; -export './src/core/api/attachment_file_uploader.dart' - show AttachmentFileUploader; +export './src/core/api/attachment_file_uploader.dart'; export './src/core/api/requests.dart'; export './src/core/api/requests.dart'; export './src/core/api/responses.dart'; @@ -44,7 +44,7 @@ export 'src/core/api/attachment_file_uploader.dart' show AttachmentFileUploader; export 'src/core/api/requests.dart'; export 'src/core/api/requests.dart'; export 'src/core/api/responses.dart'; -export 'src/core/api/stream_chat_api.dart' show PushProvider; +export 'src/core/api/stream_chat_api.dart'; export 'src/core/error/error.dart'; export 'src/core/models/action.dart'; export 'src/core/models/attachment.dart'; diff --git a/packages/stream_chat/test/src/fakes.dart b/packages/stream_chat/test/src/fakes.dart index 409418cd..ddbf4887 100644 --- a/packages/stream_chat/test/src/fakes.dart +++ b/packages/stream_chat/test/src/fakes.dart @@ -8,7 +8,6 @@ import 'package:stream_chat/src/core/api/general_api.dart'; import 'package:stream_chat/src/core/api/guest_api.dart'; import 'package:stream_chat/src/core/api/message_api.dart'; import 'package:stream_chat/src/core/api/moderation_api.dart'; -import 'package:stream_chat/src/core/api/stream_chat_api.dart'; import 'package:stream_chat/src/core/api/user_api.dart'; import 'package:stream_chat/src/core/http/token.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; From f682e0d310d88ba5542671c8766610149fa4efaf Mon Sep 17 00:00:00 2001 From: monhelle <42684014+monhelle@users.noreply.github.com> Date: Thu, 7 Jul 2022 10:07:18 +0200 Subject: [PATCH 08/49] feat(localization): Create stream_chat_localizations_no.dart (#1218) * Create stream_chat_localizations_no.dart Added the Norwegian translation for the Stream Chat * Apply suggestions from code review * Two spelling errors sneaked in * fix(localization): cleanup pr * chore(localization): update readme * chore(localization): update changelog Co-authored-by: Sahil Kumar Co-authored-by: Salvatore Giordano --- .../stream_chat_localizations/CHANGELOG.md | 4 + packages/stream_chat_localizations/README.md | 5 + .../lib/src/stream_chat_localizations.dart | 5 + .../lib/src/stream_chat_localizations_no.dart | 392 ++++++++++++++++++ 4 files changed, 406 insertions(+) create mode 100644 packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart diff --git a/packages/stream_chat_localizations/CHANGELOG.md b/packages/stream_chat_localizations/CHANGELOG.md index 1c2bc12b..aea4e155 100644 --- a/packages/stream_chat_localizations/CHANGELOG.md +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -1,3 +1,7 @@ +## Upcoming + +* Added support for [Norwegian](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart) locale. + ## 3.2.0 ✅ Added diff --git a/packages/stream_chat_localizations/README.md b/packages/stream_chat_localizations/README.md index 24805d6e..b55eb7ad 100644 --- a/packages/stream_chat_localizations/README.md +++ b/packages/stream_chat_localizations/README.md @@ -39,6 +39,7 @@ At the moment we support the following languages: - [Korean](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart) - [Portuguese](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart) - [German](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart) +- [Norwegian](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart) More languages will be added in the future. Feel free to [contribute](https://github.com/GetStream/stream-chat-flutter/blob/master/CONTRIBUTING.md) to add more languages. @@ -77,6 +78,8 @@ class MyApp extends StatelessWidget { Locale('ja'), Locale('ko'), Locale('pt'), + Locale('de'), + Locale('no'), ], // Add GlobalStreamChatLocalizations.delegates localizationsDelegates: GlobalStreamChatLocalizations.delegates, @@ -123,6 +126,8 @@ Example: ja ko pt + de + no ``` diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart index 3087e900..cda64eed 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart @@ -21,6 +21,8 @@ part 'stream_chat_localizations_pt.dart'; part 'stream_chat_localizations_de.dart'; +part 'stream_chat_localizations_no.dart'; + /// The set of supported languages, as language code strings. /// /// The [GlobalStreamChatLocalizations.delegate] can generate localizations for @@ -39,6 +41,7 @@ const kStreamChatSupportedLanguages = { 'ko', 'pt', 'de', + 'no', }; /// Creates a [GlobalStreamChatLocalizations] instance for the given `locale`. @@ -79,6 +82,8 @@ GlobalStreamChatLocalizations? getStreamChatTranslation(Locale locale) { return const StreamChatLocalizationsPt(); case 'de': return const StreamChatLocalizationsDe(); + case 'no': + return const StreamChatLocalizationsNo(); default: return null; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart new file mode 100644 index 00000000..f0abb76a --- /dev/null +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart @@ -0,0 +1,392 @@ +part of 'stream_chat_localizations.dart'; + +/// The translations for Norwegian (`no`). +class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { + /// Create an instance of the translation bundle for Norwegian. + const StreamChatLocalizationsNo({super.localeName = 'no'}); + + @override + String get launchUrlError => 'Kan ikke laste inn url'; + + @override + String get loadingUsersError => 'Problem med å laste inn brukere'; + + @override + String get noUsersLabel => 'Det er ingen brukere akkurat nå'; + + @override + String get retryLabel => 'Prøv igjen'; + + @override + String get userLastOnlineText => 'Sist pålogget'; + + @override + String get userOnlineText => 'Pålogget'; + + @override + String userTypingText(Iterable users) { + if (users.isEmpty) return ''; + final first = users.first; + if (users.length == 1) { + return '${first.name} skriver'; + } + return '${first.name} og ${users.length - 1} flere skriver'; + } + + @override + String get threadReplyLabel => 'Svar på tråd'; + + @override + String get onlyVisibleToYouText => 'Kun synlig for deg'; + + @override + String threadReplyCountText(int count) => '$count svar på tråd'; + + @override + String attachmentsUploadProgressText({ + required int remaining, + required int total, + }) => + 'Laster opp $remaining/$total ...'; + + @override + String pinnedByUserText({ + required User pinnedBy, + required User currentUser, + }) { + final pinnedByCurrentUser = currentUser.id == pinnedBy.id; + if (pinnedByCurrentUser) return 'Festet av deg'; + return 'Festet av ${pinnedBy.name}'; + } + + @override + String get sendMessagePermissionError => + 'Du har ikke tillatelse til å sende meldinger'; + + @override + String get emptyMessagesText => 'Det er ingen meldinger akkurat nå'; + + @override + String get genericErrorText => 'Noe gikk galt'; + + @override + String get loadingMessagesError => 'Problem med å laste inn meldinger'; + + @override + String resultCountText(int count) => '$count resultater'; + + @override + String get messageDeletedText => 'Denne meldingen er slettet.'; + + @override + String get messageDeletedLabel => 'Melding slettet'; + + @override + String get messageReactionsLabel => 'Reaksjoner på melding'; + + @override + String get emptyChatMessagesText => 'Ingen meldinger her enda...'; + + @override + String threadSeparatorText(int replyCount) { + if (replyCount == 1) return '1 svar'; + return '$replyCount svar'; + } + + @override + String get connectedLabel => 'Tilkoblet'; + + @override + String get disconnectedLabel => 'Avbrutt'; + + @override + String get reconnectingLabel => 'Prøver å koble til...'; + + @override + String get alsoSendAsDirectMessageLabel => 'Også send som en direktemelding'; + + @override + String get addACommentOrSendLabel => 'Legg til en kommentar eller send'; + + @override + String get searchGifLabel => 'Søk GIFs'; + + @override + String get writeAMessageLabel => 'Skriv en melding'; + + @override + String get instantCommandsLabel => 'Direkte kommandoer'; + + @override + String fileTooLargeAfterCompressionError(double limitInMB) => + 'Filen er for stor til å laste opp. ' + 'Grensen for filopplasting er $limitInMB MB. ' + 'Vi prøvde å komprimere den, men det hjalp ikke.'; + + @override + String fileTooLargeError(double limitInMB) => + 'Filen er for stor til å laste opp. Filgrense er $limitInMB MB.'; + + @override + String emojiMatchingQueryText(String query) => 'Emoji matcher "$query"'; + + @override + String get addAFileLabel => 'Legg til en fil'; + + @override + String get photoFromCameraLabel => 'Bilde fra kamera'; + + @override + String get uploadAFileLabel => 'Last opp en fil'; + + @override + String get uploadAPhotoLabel => 'Last opp et bilde'; + + @override + String get uploadAVideoLabel => 'Last opp en video'; + + @override + String get videoFromCameraLabel => 'Video fra kamera'; + + @override + String get okLabel => 'OK'; + + @override + String get somethingWentWrongError => 'Noe gikk galt'; + + @override + String get addMoreFilesLabel => 'Legg til flere filer'; + + @override + String get enablePhotoAndVideoAccessMessage => + 'Vennligst gi tillatelse til dine bilder' + '\nog videoer så du kan dele de med dine venner.'; + + @override + String get allowGalleryAccessMessage => 'Tillat tilgang til galleri'; + + @override + String get flagMessageLabel => 'Rapporter melding'; + + @override + String get flagMessageQuestion => + 'Ønsker du å sende en kopi av denne meldingen til en' + '\nmoderator for videre undersøkelser'; + + @override + String get flagLabel => 'RAPPORTER'; + + @override + String get cancelLabel => 'AVBRYT'; + + @override + String get flagMessageSuccessfulLabel => 'Melding rapportert'; + + @override + String get flagMessageSuccessfulText => + 'Meldingen har blitt rapportert til en moderator.'; + + @override + String get deleteLabel => 'SLETT'; + + @override + String get deleteMessageLabel => 'Slett melding'; + + @override + String get deleteMessageQuestion => + 'Er du sikker på at du ønsker å slette denne meldingen permanent?'; + + @override + String get operationCouldNotBeCompletedText => + 'Denne handlingen kunne ikke bli gjennomført.'; + + @override + String get replyLabel => 'Svar'; + + @override + String togglePinUnpinText({required bool pinned}) { + if (pinned) return 'Løsne fra samtale'; + return 'Fest til samtale'; + } + + @override + String toggleDeleteRetryDeleteMessageText({required bool isDeleteFailed}) { + if (isDeleteFailed) return 'Prøv å slett melding på nytt'; + return 'Slett melding'; + } + + @override + String get copyMessageLabel => 'Kopier melding'; + + @override + String get editMessageLabel => 'Rediger melding'; + + @override + String toggleResendOrResendEditedMessage({required bool isUpdateFailed}) { + if (isUpdateFailed) return 'Send redigert melding på nytt'; + return 'Send på nytt'; + } + + @override + String get photosLabel => 'Foto'; + + String _getDay(DateTime dateTime) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final yesterday = DateTime(now.year, now.month, now.day - 1); + + final date = DateTime(dateTime.year, dateTime.month, dateTime.day); + + if (date == today) { + return 'i dag'; + } else if (date == yesterday) { + return 'i går'; + } else { + return 'på ${Jiffy(date).MMMd}'; + } + } + + @override + String sentAtText({required DateTime date, required DateTime time}) => + 'Sent ${_getDay(date)} kl. ${Jiffy(time.toLocal()).format('HH:mm')}'; + + @override + String get todayLabel => 'I dag'; + + @override + String get yesterdayLabel => 'I går'; + + @override + String get channelIsMutedText => 'Kanal er dempet'; + + @override + String get noTitleText => 'Ingen tittel'; + + @override + String get letsStartChattingLabel => 'La oss starte å chatte!'; + + @override + String get sendingFirstMessageLabel => + 'Hva med å sende din første melding til en venn?'; + + @override + String get startAChatLabel => 'Start en chat'; + + @override + String get loadingChannelsError => 'Problemer med å laste inn kanaler'; + + @override + String get deleteConversationLabel => 'Slett samtale'; + + @override + String get deleteConversationQuestion => + 'Er du sikker på at du ønsker å slette denne samtalen?'; + + @override + String get streamChatLabel => 'Stream Chat'; + + @override + String get searchingForNetworkText => 'Søker etter nettverk'; + + @override + String get offlineLabel => 'Avlogget...'; + + @override + String get tryAgainLabel => 'Prøv igjen'; + + @override + String membersCountText(int count) { + if (count == 1) return '1 medlem'; + return '$count medlemmer'; + } + + @override + String watchersCountText(int count) { + if (count == 1) return '1 pålogget'; + return '$count pålogget'; + } + + @override + String get viewInfoLabel => 'Se info'; + + @override + String get leaveGroupLabel => 'Forlat gruppe'; + + @override + String get leaveLabel => 'FORLAT'; + + @override + String get leaveConversationLabel => 'Forlat samtale'; + + @override + String get leaveConversationQuestion => + 'Er du sikker på at du ønsker å forlate denne samtalen?'; + + @override + String get showInChatLabel => 'Se i chat'; + + @override + String get saveImageLabel => 'Lagre bilde'; + + @override + String get saveVideoLabel => 'Lagre video'; + + @override + String get uploadErrorLabel => 'PROBLEM MED OPPLASTNING'; + + @override + String get giphyLabel => 'Giphy'; + + @override + String get shuffleLabel => 'Stokk om'; + + @override + String get sendLabel => 'Send'; + + @override + String get withText => 'med'; + + @override + String get inText => 'i'; + + @override + String get youText => 'Du'; + + @override + String galleryPaginationText({ + required int currentPage, + required int totalPages, + }) => + '${currentPage + 1} of $totalPages'; + + @override + String get fileText => 'Fil'; + + @override + String get replyToMessageLabel => 'Svar på melding'; + + @override + String attachmentLimitExceedError(int limit) => + 'Antall vedlegg oversteget, maks antall: $limit'; + + @override + String get slowModeOnLabel => 'Sakte modus PÅ'; + + @override + String get linkDisabledDetails => + 'Sende lenker er ikke lov i denne samtalen.'; + + @override + String get linkDisabledError => 'Lenker er deaktivert'; + + @override + String get viewLibrary => 'Se bibliotek'; + + @override + String unreadMessagesSeparatorText(int unreadCount) { + if (unreadCount == 1) { + return '1 ulest melding'; + } + return '$unreadCount uleste meldinger'; + } +} From 6ab94a8aaa145bcc2c010f54cfab48213d044ddd Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 7 Jul 2022 10:46:36 +0200 Subject: [PATCH 09/49] chore(llc,core,ui,persistence,localizations): update changelog and pubspec (#1253) --- packages/stream_chat/CHANGELOG.md | 2 +- packages/stream_chat/pubspec.yaml | 2 +- packages/stream_chat_flutter/CHANGELOG.md | 2 +- packages/stream_chat_flutter/pubspec.yaml | 4 ++-- packages/stream_chat_flutter_core/CHANGELOG.md | 4 ++++ packages/stream_chat_flutter_core/example/pubspec.yaml | 2 +- packages/stream_chat_flutter_core/pubspec.yaml | 4 ++-- packages/stream_chat_localizations/CHANGELOG.md | 2 +- packages/stream_chat_localizations/pubspec.yaml | 4 ++-- packages/stream_chat_persistence/CHANGELOG.md | 4 ++++ packages/stream_chat_persistence/example/pubspec.yaml | 4 ++-- packages/stream_chat_persistence/pubspec.yaml | 4 ++-- 12 files changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 11e630fb..b934eeb1 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,4 +1,4 @@ -## Upcoming +## 4.4.0 🐞 Fixed diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index 7c5968cc..2704233a 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat homepage: https://getstream.io/ description: The official Dart client for Stream Chat, a service for building chat applications. -version: 4.3.0 +version: 4.4.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 10b85eb3..08fcb15d 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,4 +1,4 @@ -## Upcoming +## 4.4.0 🐞 Fixed diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 478be89d..98541e17 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter. -version: 4.3.0 +version: 4.4.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -36,7 +36,7 @@ dependencies: rxdart: ^0.27.0 share_plus: ^4.0.1 shimmer: ^2.0.0 - stream_chat_flutter_core: ^4.3.0 + stream_chat_flutter_core: ^4.4.0 substring_highlight: ^1.0.26 url_launcher: ^6.1.0 video_player: ^2.1.0 diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index b986bbd6..a0e612b8 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.4.0 + +- Updated `stream_chat` dependency to [`4.4.0`](https://pub.dev/packages/stream_chat/changelog). + ## 4.3.0 - Updated `stream_chat` dependency to [`4.3.0`](https://pub.dev/packages/stream_chat/changelog). diff --git a/packages/stream_chat_flutter_core/example/pubspec.yaml b/packages/stream_chat_flutter_core/example/pubspec.yaml index ce91a8f4..a78fa6a0 100644 --- a/packages/stream_chat_flutter_core/example/pubspec.yaml +++ b/packages/stream_chat_flutter_core/example/pubspec.yaml @@ -26,7 +26,7 @@ dependencies: cupertino_icons: ^1.0.3 flutter: sdk: flutter - stream_chat_flutter_core: ^4.3.0 + stream_chat_flutter_core: ^4.4.0 dev_dependencies: flutter_test: sdk: flutter diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index 3365e724..7317c758 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter_core homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter. -version: 4.3.0 +version: 4.4.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -17,7 +17,7 @@ dependencies: freezed_annotation: ^2.0.3 meta: ^1.3.0 rxdart: ^0.27.0 - stream_chat: ^4.3.0 + stream_chat: ^4.4.0 dev_dependencies: build_runner: ^2.0.1 dart_code_metrics: ^4.4.0 diff --git a/packages/stream_chat_localizations/CHANGELOG.md b/packages/stream_chat_localizations/CHANGELOG.md index aea4e155..850fba35 100644 --- a/packages/stream_chat_localizations/CHANGELOG.md +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -1,4 +1,4 @@ -## Upcoming +## 3.3.0 * Added support for [Norwegian](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart) locale. diff --git a/packages/stream_chat_localizations/pubspec.yaml b/packages/stream_chat_localizations/pubspec.yaml index caa85279..f507b4a6 100644 --- a/packages/stream_chat_localizations/pubspec.yaml +++ b/packages/stream_chat_localizations/pubspec.yaml @@ -1,6 +1,6 @@ name: stream_chat_localizations description: The Official localizations for Stream Chat Flutter, a service for building chat applications -version: 3.2.0 +version: 3.3.0 homepage: https://github.com/GetStream/stream-chat-flutter repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -14,7 +14,7 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - stream_chat_flutter: ^4.3.0 + stream_chat_flutter: ^4.4.0 dev_dependencies: dart_code_metrics: ^4.4.0 flutter_test: diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index 4c9bea04..a10947a2 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.3.0 + +- Updated `stream_chat` dependency to [`4.4.0`](https://pub.dev/packages/stream_chat/changelog). + ## 4.2.0 - Added support for `Channel.ownCapabilities` diff --git a/packages/stream_chat_persistence/example/pubspec.yaml b/packages/stream_chat_persistence/example/pubspec.yaml index 26830534..af431a6f 100644 --- a/packages/stream_chat_persistence/example/pubspec.yaml +++ b/packages/stream_chat_persistence/example/pubspec.yaml @@ -11,8 +11,8 @@ dependencies: cupertino_icons: ^1.0.3 flutter: sdk: flutter - stream_chat: ^4.3.0 - stream_chat_persistence: ^4.2.0 + stream_chat: ^4.4.0 + stream_chat_persistence: ^4.3.0 dev_dependencies: flutter_test: sdk: flutter diff --git a/packages/stream_chat_persistence/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index 065be5cc..9b0dfef7 100644 --- a/packages/stream_chat_persistence/pubspec.yaml +++ b/packages/stream_chat_persistence/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_persistence homepage: https://github.com/GetStream/stream-chat-flutter description: Official Stream Chat Persistence library. Build your own chat experience using Dart and Flutter. -version: 4.2.0 +version: 4.3.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -19,7 +19,7 @@ dependencies: path: ^1.8.0 path_provider: ^2.0.1 sqlite3_flutter_libs: ^0.5.0 - stream_chat: ^4.3.0 + stream_chat: ^4.4.0 dev_dependencies: build_runner: ^2.0.1 dart_code_metrics: ^4.4.0 From d1bfe7e2f8f47d1e649b2eacbdab6158389eb753 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 7 Jul 2022 12:05:23 +0200 Subject: [PATCH 10/49] update version.dart --- packages/stream_chat/lib/version.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/lib/version.dart b/packages/stream_chat/lib/version.dart index faafb093..9acb4e16 100644 --- a/packages/stream_chat/lib/version.dart +++ b/packages/stream_chat/lib/version.dart @@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart'; /// Current package version /// Used in [StreamChatClient] to build the `x-stream-client` header // ignore: constant_identifier_names -const PACKAGE_VERSION = '4.3.0'; +const PACKAGE_VERSION = '4.4.0'; From 482d4ed611fb44926b88b2fab999889cf6523349 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 7 Jul 2022 12:19:32 +0200 Subject: [PATCH 11/49] fix(ui): fix jiffy locale initialization (#1255) * fix(ui): fix jiffy locale initialization * chore(ui): update changelog * fix(ui): cleanup --- packages/stream_chat_flutter/CHANGELOG.md | 6 ++++++ packages/stream_chat_flutter/lib/src/stream_chat.dart | 7 +++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 08fcb15d..23950e3d 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcming + +🐞 Fixed + +- [[#1247]](https://github.com/GetStream/stream-chat-flutter/issues/1247) Fix Jiffy initialization. + ## 4.4.0 🐞 Fixed diff --git a/packages/stream_chat_flutter/lib/src/stream_chat.dart b/packages/stream_chat_flutter/lib/src/stream_chat.dart index 54bd7191..cef33020 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat.dart @@ -140,11 +140,10 @@ class StreamChatState extends State { @override void didChangeDependencies() { - final currentLocale = Localizations.localeOf(context); - final languageCode = currentLocale.languageCode; + final currentLocale = Localizations.localeOf(context).toString(); final availableLocales = Jiffy.getAllAvailableLocales(); - if (availableLocales.contains(languageCode)) { - Jiffy.locale(languageCode); + if (availableLocales.contains(currentLocale)) { + Jiffy.locale(currentLocale); } super.didChangeDependencies(); } From 0cffd094151742dcb50b5b28243488d15ba29696 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 11 Jul 2022 09:58:43 +0200 Subject: [PATCH 12/49] fix(ui): fix date divider not showing (#1259) * fix(ui): fix date divider not showing * chore(ui): update changelog * chore(ui): minor changes Signed-off-by: xsahil03x * fix(ui): fix separator Signed-off-by: xsahil03x Co-authored-by: Sahil Kumar --- packages/stream_chat_flutter/CHANGELOG.md | 1 + .../lib/src/message_list_view.dart | 138 +++++++++--------- 2 files changed, 71 insertions(+), 68 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 23950e3d..1d0d0bde 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -3,6 +3,7 @@ 🐞 Fixed - [[#1247]](https://github.com/GetStream/stream-chat-flutter/issues/1247) Fix Jiffy initialization. +- [[#1232]](https://github.com/getstream/stream-chat-flutter/issues/1232) Fix DateDivider not showing up in the chat. ## 4.4.0 diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 31853b90..af54d5ee 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -206,7 +206,7 @@ class StreamMessageListView extends StatefulWidget { this.paginationLimit = 20, this.paginationLoadingIndicatorBuilder, this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.onDrag, - this.spacingWidgetBuilder, + this.spacingWidgetBuilder = _defaultSpacingWidgetBuilder, }); /// [ScrollViewKeyboardDismissBehavior] the defines how this [PositionedList] will @@ -353,7 +353,17 @@ class StreamMessageListView extends StatefulWidget { /// A List of [SpacingType] is provided to provide more data about the /// type of message (thread, difference in time between current and last /// message, default spacing, etc) - final SpacingWidgetBuilder? spacingWidgetBuilder; + final SpacingWidgetBuilder spacingWidgetBuilder; + + static Widget _defaultSpacingWidgetBuilder( + BuildContext context, + List spacingTypes, + ) { + if (!spacingTypes.contains(SpacingType.defaultSpacing)) { + return const SizedBox(height: 8); + } + return const SizedBox(height: 2); + } @override _StreamMessageListViewState createState() => _StreamMessageListViewState(); @@ -612,7 +622,6 @@ class _StreamMessageListViewState extends State { } late final Message message, nextMessage; - late Widget separator; if (widget.reverse) { message = messages[i - 1]; nextMessage = messages[i - 2]; @@ -621,84 +630,53 @@ class _StreamMessageListViewState extends State { nextMessage = messages[i - 1]; } + Widget separator; + + final isThread = message.replyCount! > 0; + if (!Jiffy(message.createdAt.toLocal()).isSame( nextMessage.createdAt.toLocal(), Units.DAY, )) { separator = _buildDateDivider(nextMessage); + } else { + final timeDiff = + Jiffy(nextMessage.createdAt.toLocal()).diff( + message.createdAt.toLocal(), + Units.MINUTE, + ); + + final isNextUserSame = + message.user!.id == nextMessage.user?.id; + final isDeleted = message.isDeleted; + final hasTimeDiff = timeDiff >= 1; + + final spacingRules = [ + if (hasTimeDiff) SpacingType.timeDiff, + if (!isNextUserSame) SpacingType.otherUser, + if (isThread) SpacingType.thread, + if (isDeleted) SpacingType.deleted, + ]; + + if (spacingRules.isEmpty) { + spacingRules.add(SpacingType.defaultSpacing); + } + + separator = widget.spacingWidgetBuilder.call( + context, + spacingRules, + ); } - final timeDiff = - Jiffy(nextMessage.createdAt.toLocal()).diff( - message.createdAt.toLocal(), - Units.MINUTE, - ); - - final spacingRules = []; - - final isNextUserSame = - message.user!.id == nextMessage.user?.id; - final isThread = message.replyCount! > 0; - final isDeleted = message.isDeleted; - final hasTimeDiff = timeDiff >= 1; - - if (hasTimeDiff) { - spacingRules.add(SpacingType.timeDiff); - } - - if (!isNextUserSame) { - spacingRules.add(SpacingType.otherUser); - } - - if (isThread) { - spacingRules.add(SpacingType.thread); - } - - if (isDeleted) { - spacingRules.add(SpacingType.deleted); - } - - if (spacingRules.isNotEmpty) { - separator = widget.spacingWidgetBuilder - ?.call(context, spacingRules) ?? - const SizedBox(height: 8); - } - separator = widget.spacingWidgetBuilder - ?.call(context, [SpacingType.defaultSpacing]) ?? - const SizedBox(height: 2); if (!isThread && unreadCount > 0 && unreadCount == i - 1) { - final unreadMessagesSeparator = widget - .unreadMessagesSeparatorBuilder - ?.call(context, unreadCount); + final unreadMessagesSeparator = + _buildUnreadMessagesSeparator(unreadCount); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ separator, - unreadMessagesSeparator ?? - Padding( - padding: - const EdgeInsets.symmetric(vertical: 8), - child: DecoratedBox( - decoration: BoxDecoration( - gradient: - _streamTheme.colorTheme.bgGradient, - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: Text( - context.translations - .unreadMessagesSeparatorText( - unreadCount, - ), - textAlign: TextAlign.center, - style: - StreamChannelHeaderTheme.of(context) - .subtitleStyle, - ), - ), - ), - ), + unreadMessagesSeparator, ], ); } @@ -816,6 +794,30 @@ class _StreamMessageListViewState extends State { return child; } + Widget _buildUnreadMessagesSeparator(int unreadCount) { + final unreadMessagesSeparator = + widget.unreadMessagesSeparatorBuilder?.call(context, unreadCount) ?? + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: DecoratedBox( + decoration: BoxDecoration( + gradient: _streamTheme.colorTheme.bgGradient, + ), + child: Padding( + padding: const EdgeInsets.all(8), + child: Text( + context.translations.unreadMessagesSeparatorText( + unreadCount, + ), + textAlign: TextAlign.center, + style: StreamChannelHeaderTheme.of(context).subtitleStyle, + ), + ), + ), + ); + return unreadMessagesSeparator; + } + Widget _buildDateDivider(Message message) { final divider = widget.dateDividerBuilder != null ? widget.dateDividerBuilder!( From 5acd1c1defac70bc9bca220fc01dda2c7f93072b Mon Sep 17 00:00:00 2001 From: Gordon Date: Fri, 15 Jul 2022 15:40:39 +0200 Subject: [PATCH 13/49] docs: add filter to StreamUserListController (#1267) The filter is a required field for the API call --- .../stream_chat_flutter_core/stream_user_list_controller.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_user_list_controller.mdx b/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_user_list_controller.mdx index 0bee29ed..885b1c82 100644 --- a/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_user_list_controller.mdx +++ b/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_user_list_controller.mdx @@ -27,6 +27,7 @@ class UserListPageState extends State { /// [StreamUserListController]. late final userListController = StreamUserListController( client: StreamChatCore.of(context).client, + filter: Filter.notEqual('id', StreamChatCore.of(context).currentUser!), ); ``` @@ -109,4 +110,4 @@ Widget build(BuildContext context) => Scaffold( ); ``` -In this case we're using the [`LazyLoadScrollView`](./lazy_load_scroll_view.mdx) widget to load more data when the user scrolls to the bottom of the list. \ No newline at end of file +In this case we're using the [`LazyLoadScrollView`](./lazy_load_scroll_view.mdx) widget to load more data when the user scrolls to the bottom of the list. From feee8581c87d272fb4bb367e7e4f933915f9cc30 Mon Sep 17 00:00:00 2001 From: Dmitry Krutskikh Date: Mon, 18 Jul 2022 14:40:39 +0300 Subject: [PATCH 14/49] chore: removed duplicate exports (#1268) --- packages/stream_chat/lib/stream_chat.dart | 3 --- packages/stream_chat_flutter/lib/stream_chat_flutter.dart | 2 -- 2 files changed, 5 deletions(-) diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 27ba68cc..2755224f 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -5,14 +5,12 @@ export 'package:dio/src/cancel_token.dart'; export 'package:dio/src/dio_error.dart'; export 'package:dio/src/multipart_file.dart'; export 'package:dio/src/options.dart'; -export 'package:dio/src/options.dart' show ProgressCallback; export 'package:logging/logging.dart' show Logger, Level, LogRecord; export 'package:rate_limiter/rate_limiter.dart'; export 'package:uuid/uuid.dart'; export './src/core/api/attachment_file_uploader.dart'; export './src/core/api/requests.dart'; -export './src/core/api/requests.dart'; export './src/core/api/responses.dart'; export './src/core/api/stream_chat_api.dart' show PushProvider; export './src/core/error/error.dart'; @@ -42,7 +40,6 @@ export 'src/client/channel.dart'; export 'src/client/client.dart'; export 'src/core/api/attachment_file_uploader.dart' show AttachmentFileUploader; export 'src/core/api/requests.dart'; -export 'src/core/api/requests.dart'; export 'src/core/api/responses.dart'; export 'src/core/api/stream_chat_api.dart'; export 'src/core/error/error.dart'; diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 5052393a..39023c85 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -65,8 +65,6 @@ export 'src/v4/scroll_view/message_search_scroll_view/stream_message_search_list export 'src/v4/scroll_view/stream_scroll_view_empty_widget.dart'; export 'src/v4/scroll_view/stream_scroll_view_indexed_widget_builder.dart'; export 'src/v4/scroll_view/user_scroll_view/stream_user_grid_tile.dart'; -export 'src/v4/scroll_view/user_scroll_view/stream_user_grid_tile.dart'; -export 'src/v4/scroll_view/user_scroll_view/stream_user_grid_view.dart'; export 'src/v4/scroll_view/user_scroll_view/stream_user_grid_view.dart'; export 'src/v4/scroll_view/user_scroll_view/stream_user_list_tile.dart'; export 'src/v4/scroll_view/user_scroll_view/stream_user_list_view.dart'; From bbb22f891b94a9f576a8fd4f237656e63df695fc Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 20 Jul 2022 22:24:14 +0530 Subject: [PATCH 15/49] Update CHANGELOG.md --- packages/stream_chat_persistence/CHANGELOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index 5ed8c46c..a330933c 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,3 +1,8 @@ +## Upcoming + +- Allowed experimental use of indexedDb on web with `webUseExperimentalIndexedDb` parameter on `StreamChatPersistenceClient`. + Thanks [geweald](https://github.com/geweald). + ## 4.3.0 - Updated `stream_chat` dependency to [`4.4.0`](https://pub.dev/packages/stream_chat/changelog). @@ -5,7 +10,6 @@ ## 4.2.0 - Added support for `Channel.ownCapabilities` -- Allowed experimental use of indexedDb on web with `webUseExperimentalIndexedDb` parameter on `StreamChatPersistenceClient` ## 4.1.0 @@ -124,4 +128,4 @@ ## 1.0.0-beta -* Initial release \ No newline at end of file +* Initial release From bf8630068bc53200deb068fb47588ff834cd9cfc Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 22 Jul 2022 15:34:32 +0530 Subject: [PATCH 16/49] fix(ui): substitute mentioned user ids with user names in system message (#1278) Signed-off-by: xsahil03x --- packages/stream_chat_flutter/CHANGELOG.md | 5 ++++- packages/stream_chat_flutter/lib/src/system_message.dart | 9 ++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 1d0d0bde..4d1b6644 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -3,7 +3,10 @@ 🐞 Fixed - [[#1247]](https://github.com/GetStream/stream-chat-flutter/issues/1247) Fix Jiffy initialization. -- [[#1232]](https://github.com/getstream/stream-chat-flutter/issues/1232) Fix DateDivider not showing up in the chat. +- [[#1232]](https://github.com/getstream/stream-chat-flutter/issues/1232) Fix DateDivider not + showing up in the chat. +- [[#1240]](https://github.com/getstream/stream-chat-flutter/issues/1240) Substitute mentioned user + ids with user names in system message. ## 4.4.0 diff --git a/packages/stream_chat_flutter/lib/src/system_message.dart b/packages/stream_chat_flutter/lib/src/system_message.dart index 1f720db2..6135afc0 100644 --- a/packages/stream_chat_flutter/lib/src/system_message.dart +++ b/packages/stream_chat_flutter/lib/src/system_message.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// {@macro system_message} @@ -21,16 +22,22 @@ class StreamSystemMessage extends StatelessWidget { /// The function called when tapping on the message /// when the message is not failed + // TODO: Convert it to a simple OnTap instead. final void Function(Message)? onMessageTap; @override Widget build(BuildContext context) { final theme = StreamChatTheme.of(context); + final message = this.message.replaceMentions(); + + final messageText = message.text; + if (messageText == null) return const SizedBox.shrink(); + return GestureDetector( behavior: HitTestBehavior.opaque, onTap: onMessageTap == null ? null : () => onMessageTap!(message), child: Text( - message.text!, + messageText, textAlign: TextAlign.center, softWrap: true, style: theme.textTheme.captionBold.copyWith( From a90638adb1fd0f927d95b257abf2189d64899159 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 22 Jul 2022 16:19:08 +0200 Subject: [PATCH 17/49] fix(ui): export kDefaultMaxAttachmentSize and use it in `StreamAttachmentPicker` default, also increase it to 100Mb --- packages/stream_chat_flutter/lib/src/message_input.dart | 5 +++-- .../lib/src/v4/message_input/stream_attachment_picker.dart | 2 +- .../lib/src/v4/message_input/stream_message_input.dart | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 4ade7890..76031e5f 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -110,7 +110,8 @@ enum SendButtonLocation { const _kMinMediaPickerSize = 360.0; -const _kDefaultMaxAttachmentSize = 20971520; // 20MB in Bytes +/// Default maximum size for media attachments. +const kDefaultMaxAttachmentSize = 1024 * 1024 * 100; // 100MB in Bytes /// Inactive state /// @@ -188,7 +189,7 @@ class MessageInput extends StatefulWidget { @Deprecated('''Use `userMentionsTileBuilder` instead. Will be removed in future release''') this.mentionsTileBuilder, this.userMentionsTileBuilder, - this.maxAttachmentSize = _kDefaultMaxAttachmentSize, + this.maxAttachmentSize = kDefaultMaxAttachmentSize, this.onError, this.attachmentLimit = 10, this.onAttachmentLimitExceed, diff --git a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart index bea16577..9f10bae7 100644 --- a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart +++ b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart @@ -32,7 +32,7 @@ class StreamAttachmentPicker extends StatefulWidget { this.pickerSize = 360.0, this.attachmentLimit = 10, this.onAttachmentLimitExceeded, - this.maxAttachmentSize = 20971520, + this.maxAttachmentSize = kDefaultMaxAttachmentSize, this.onError, this.allowedAttachmentTypes = const [ DefaultAttachmentTypes.image, diff --git a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart index f577e8d9..6ea941ba 100644 --- a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart +++ b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart @@ -128,7 +128,8 @@ enum SendButtonLocation { const _kMinMediaPickerSize = 360.0; -const _kDefaultMaxAttachmentSize = 20971520; // 20MB in Bytes +/// Default maximum size for media attachments. +const kDefaultMaxAttachmentSize = 1024 * 1024 * 100; // 100MB in Bytes /// Inactive state: /// From f9f608ffc3a35b4b6c29987199a9d492897bf11c Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 22 Jul 2022 16:19:45 +0200 Subject: [PATCH 18/49] chore(ui): update changelog --- packages/stream_chat_flutter/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 4d1b6644..ac26b112 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -8,6 +8,10 @@ - [[#1240]](https://github.com/getstream/stream-chat-flutter/issues/1240) Substitute mentioned user ids with user names in system message. +🔄 Changed + +- Changed default maximum attachment size from 20MB to 100MB. + ## 4.4.0 🐞 Fixed From b5414349e7a5e4bdf78a7bd0078a0f9039b59191 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 25 Jul 2022 10:53:51 +0200 Subject: [PATCH 19/49] fix(ui): rename const --- .../lib/src/v4/message_input/stream_message_input.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart index 6ea941ba..56ffcfe1 100644 --- a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart +++ b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart @@ -194,7 +194,7 @@ class StreamMessageInput extends StatefulWidget { this.activeSendButton, this.showCommandsButton = true, this.userMentionsTileBuilder, - this.maxAttachmentSize = _kDefaultMaxAttachmentSize, + this.maxAttachmentSize = kDefaultMaxAttachmentSize, this.onError, this.attachmentLimit = 10, this.onAttachmentLimitExceed, From e9e35d7d292d7e0295ae5e7ee5c4a40642d15019 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 25 Jul 2022 11:31:45 +0200 Subject: [PATCH 20/49] fix(ui): fix image download on iOS (#1280) * fix(ui): fix image download * chore(ui): update changelog * Update packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart * Update packages/stream_chat_flutter/CHANGELOG.md Co-authored-by: Deven Joshi Co-authored-by: Deven Joshi --- packages/stream_chat_flutter/CHANGELOG.md | 3 ++- .../stream_chat_flutter/lib/src/attachment_actions_modal.dart | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index ac26b112..a2f1dd2d 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,4 +1,4 @@ -## Upcming +## Upcoming 🐞 Fixed @@ -7,6 +7,7 @@ showing up in the chat. - [[#1240]](https://github.com/getstream/stream-chat-flutter/issues/1240) Substitute mentioned user ids with user names in system message. +- [[#1228]](https://github.com/GetStream/stream-chat-flutter/issues/1228) Fix image download on iOS. 🔄 Changed diff --git a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart index a3d60483..5c2fdb17 100644 --- a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart @@ -405,7 +405,7 @@ class AttachmentActionsModal extends StatelessWidget { onReceiveProgress: progressCallback, ); final result = await ImageGallerySaver.saveFile(filePath!); - downloadedPathCallback?.call((result as Map)['filePath']); + downloadedPathCallback?.call((result as Map)['filePath'] ?? filePath); return (result as Map)['filePath']; } } From 89e160969d2d8be662f3f4f939eb2e4e1c56282d Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 27 Jul 2022 10:36:11 +0200 Subject: [PATCH 21/49] fix(llc): do not serialize bytes (#1283) * fix(llc): do not serialize bytes * chore(llc): update changelog * fix(ui): cache files picked with photo_manager * chore(ui): cleanup --- packages/stream_chat/CHANGELOG.md | 6 +++++ .../lib/src/core/models/attachment_file.dart | 12 +--------- .../core/models/attachment_file.freezed.dart | 22 +++++++++++++------ .../src/core/models/attachment_file.g.dart | 2 -- .../stream_attachment_picker.dart | 17 ++++++++------ 5 files changed, 32 insertions(+), 27 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index b934eeb1..fa4d18ab 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +🐞 Fixed + +- Do not serialize `AttachmentFile.bytes` + ## 4.4.0 🐞 Fixed diff --git a/packages/stream_chat/lib/src/core/models/attachment_file.dart b/packages/stream_chat/lib/src/core/models/attachment_file.dart index 1684e005..edce0e1a 100644 --- a/packages/stream_chat/lib/src/core/models/attachment_file.dart +++ b/packages/stream_chat/lib/src/core/models/attachment_file.dart @@ -51,7 +51,7 @@ class AttachmentFile { /// Byte data for this file. Particularly useful if you want to manipulate /// its data or easily upload to somewhere else. - @JsonKey(toJson: _toString, fromJson: _fromString) + @JsonKey(ignore: true) final Uint8List? bytes; /// The file size in bytes. @@ -124,13 +124,3 @@ class UploadState with _$UploadState { /// Returns true if state is [Failed] bool get isFailed => this is Failed; } - -Uint8List? _fromString(String? bytes) { - if (bytes == null) return null; - return Uint8List.fromList(bytes.codeUnits); -} - -String? _toString(Uint8List? bytes) { - if (bytes == null) return null; - return String.fromCharCodes(bytes); -} diff --git a/packages/stream_chat/lib/src/core/models/attachment_file.freezed.dart b/packages/stream_chat/lib/src/core/models/attachment_file.freezed.dart index 2ea50924..f0ccc584 100644 --- a/packages/stream_chat/lib/src/core/models/attachment_file.freezed.dart +++ b/packages/stream_chat/lib/src/core/models/attachment_file.freezed.dart @@ -224,7 +224,9 @@ class _$Preparing extends Preparing { @override Map toJson() { - return _$$PreparingToJson(this); + return _$$PreparingToJson( + this, + ); } } @@ -392,7 +394,9 @@ class _$InProgress extends InProgress { @override Map toJson() { - return _$$InProgressToJson(this); + return _$$InProgressToJson( + this, + ); } } @@ -404,8 +408,8 @@ abstract class InProgress extends UploadState { factory InProgress.fromJson(Map json) = _$InProgress.fromJson; - int get uploaded => throw _privateConstructorUsedError; - int get total => throw _privateConstructorUsedError; + int get uploaded; + int get total; @JsonKey(ignore: true) _$$InProgressCopyWith<_$InProgress> get copyWith => throw _privateConstructorUsedError; @@ -531,7 +535,9 @@ class _$Success extends Success { @override Map toJson() { - return _$$SuccessToJson(this); + return _$$SuccessToJson( + this, + ); } } @@ -686,7 +692,9 @@ class _$Failed extends Failed { @override Map toJson() { - return _$$FailedToJson(this); + return _$$FailedToJson( + this, + ); } } @@ -696,7 +704,7 @@ abstract class Failed extends UploadState { factory Failed.fromJson(Map json) = _$Failed.fromJson; - String get error => throw _privateConstructorUsedError; + String get error; @JsonKey(ignore: true) _$$FailedCopyWith<_$Failed> get copyWith => throw _privateConstructorUsedError; diff --git a/packages/stream_chat/lib/src/core/models/attachment_file.g.dart b/packages/stream_chat/lib/src/core/models/attachment_file.g.dart index 6b657c2a..fe02433e 100644 --- a/packages/stream_chat/lib/src/core/models/attachment_file.g.dart +++ b/packages/stream_chat/lib/src/core/models/attachment_file.g.dart @@ -11,14 +11,12 @@ AttachmentFile _$AttachmentFileFromJson(Map json) => size: json['size'] as int?, path: json['path'] as String?, name: json['name'] as String?, - bytes: _fromString(json['bytes'] as String?), ); Map _$AttachmentFileToJson(AttachmentFile instance) => { 'path': instance.path, 'name': instance.name, - 'bytes': _toString(instance.bytes), 'size': instance.size, }; diff --git a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart index 9f10bae7..2473cf0a 100644 --- a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart +++ b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:photo_manager/photo_manager.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/media_list_view.dart'; @@ -369,17 +370,19 @@ class _StreamAttachmentPickerState extends State { } void _addAssetAttachment(AssetEntity medium) async { - final mediaFile = await medium.originFile.timeout( - const Duration(seconds: 5), - onTimeout: () => medium.originFile, - ); + final mediaFile = await medium.originFile; if (mediaFile == null) return; + final tempDir = await getTemporaryDirectory(); + + final cachedFile = await mediaFile + .copy('${tempDir.path}/${mediaFile.path.split('/').last}'); + final file = AttachmentFile( - path: mediaFile.path, - size: await mediaFile.length(), - bytes: mediaFile.readAsBytesSync(), + path: cachedFile.path, + size: await cachedFile.length(), + bytes: cachedFile.readAsBytesSync(), ); if (file.size! > widget.maxAttachmentSize) { From 5ad21bc3d2a61730a8ecb5f316d127d7fbd77d4f Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 28 Jul 2022 10:45:05 +0200 Subject: [PATCH 22/49] chore(llc,core,ui,persistence): bump version and changelog (#1288) * update deps * chore(llc,core,ui,persistence): bump version and changelog --- packages/stream_chat/CHANGELOG.md | 2 +- packages/stream_chat/lib/version.dart | 2 +- packages/stream_chat/pubspec.yaml | 2 +- packages/stream_chat_flutter/CHANGELOG.md | 2 +- packages/stream_chat_flutter/pubspec.yaml | 8 ++++---- packages/stream_chat_flutter_core/CHANGELOG.md | 4 ++++ packages/stream_chat_flutter_core/example/pubspec.yaml | 2 +- packages/stream_chat_flutter_core/pubspec.yaml | 5 +++-- packages/stream_chat_localizations/pubspec.yaml | 2 +- packages/stream_chat_persistence/CHANGELOG.md | 2 +- packages/stream_chat_persistence/example/pubspec.yaml | 4 ++-- packages/stream_chat_persistence/pubspec.yaml | 4 ++-- 12 files changed, 22 insertions(+), 17 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index fa4d18ab..e8a7aa12 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,4 +1,4 @@ -## Upcoming +## 4.4.1 🐞 Fixed diff --git a/packages/stream_chat/lib/version.dart b/packages/stream_chat/lib/version.dart index 9acb4e16..3deb7f2f 100644 --- a/packages/stream_chat/lib/version.dart +++ b/packages/stream_chat/lib/version.dart @@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart'; /// Current package version /// Used in [StreamChatClient] to build the `x-stream-client` header // ignore: constant_identifier_names -const PACKAGE_VERSION = '4.4.0'; +const PACKAGE_VERSION = '4.4.1'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index 2704233a..8b15b993 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat homepage: https://getstream.io/ description: The official Dart client for Stream Chat, a service for building chat applications. -version: 4.4.0 +version: 4.4.1 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index a2f1dd2d..f098c65c 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,4 +1,4 @@ -## Upcoming +## 4.4.1 🐞 Fixed diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 98541e17..5caa7614 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter. -version: 4.4.0 +version: 4.4.1 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -17,12 +17,12 @@ dependencies: diacritic: ^0.1.3 dio: ^4.0.6 ezanimation: ^0.6.0 - file_picker: ^4.1.3 + file_picker: ^5.0.0 flutter: sdk: flutter flutter_markdown: ^0.6.1 flutter_portal: ^1.0.0 - flutter_slidable: ^1.2.0 + flutter_slidable: ^2.0.0 flutter_svg: ^1.0.1 http_parser: ^4.0.0 image_gallery_saver: ^1.7.0 @@ -36,7 +36,7 @@ dependencies: rxdart: ^0.27.0 share_plus: ^4.0.1 shimmer: ^2.0.0 - stream_chat_flutter_core: ^4.4.0 + stream_chat_flutter_core: ^4.4.1 substring_highlight: ^1.0.26 url_launcher: ^6.1.0 video_player: ^2.1.0 diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index a0e612b8..ba44eb78 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.4.1 + +- Updated `stream_chat` dependency to [`4.4.1`](https://pub.dev/packages/stream_chat/changelog). + ## 4.4.0 - Updated `stream_chat` dependency to [`4.4.0`](https://pub.dev/packages/stream_chat/changelog). diff --git a/packages/stream_chat_flutter_core/example/pubspec.yaml b/packages/stream_chat_flutter_core/example/pubspec.yaml index a78fa6a0..59afbf41 100644 --- a/packages/stream_chat_flutter_core/example/pubspec.yaml +++ b/packages/stream_chat_flutter_core/example/pubspec.yaml @@ -26,7 +26,7 @@ dependencies: cupertino_icons: ^1.0.3 flutter: sdk: flutter - stream_chat_flutter_core: ^4.4.0 + stream_chat_flutter_core: ^4.4.1 dev_dependencies: flutter_test: sdk: flutter diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index 7317c758..be0171ec 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter_core homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter. -version: 4.4.0 +version: 4.4.1 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -17,7 +17,8 @@ dependencies: freezed_annotation: ^2.0.3 meta: ^1.3.0 rxdart: ^0.27.0 - stream_chat: ^4.4.0 + stream_chat: ^4.4.1 + dev_dependencies: build_runner: ^2.0.1 dart_code_metrics: ^4.4.0 diff --git a/packages/stream_chat_localizations/pubspec.yaml b/packages/stream_chat_localizations/pubspec.yaml index f507b4a6..2c819f55 100644 --- a/packages/stream_chat_localizations/pubspec.yaml +++ b/packages/stream_chat_localizations/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - stream_chat_flutter: ^4.4.0 + stream_chat_flutter: ^4.4.1 dev_dependencies: dart_code_metrics: ^4.4.0 flutter_test: diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index a330933c..3d1f0184 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,4 +1,4 @@ -## Upcoming +## 4.4.0 - Allowed experimental use of indexedDb on web with `webUseExperimentalIndexedDb` parameter on `StreamChatPersistenceClient`. Thanks [geweald](https://github.com/geweald). diff --git a/packages/stream_chat_persistence/example/pubspec.yaml b/packages/stream_chat_persistence/example/pubspec.yaml index af431a6f..3c47bd1b 100644 --- a/packages/stream_chat_persistence/example/pubspec.yaml +++ b/packages/stream_chat_persistence/example/pubspec.yaml @@ -11,8 +11,8 @@ dependencies: cupertino_icons: ^1.0.3 flutter: sdk: flutter - stream_chat: ^4.4.0 - stream_chat_persistence: ^4.3.0 + stream_chat: ^4.4.1 + stream_chat_persistence: ^4.4.0 dev_dependencies: flutter_test: sdk: flutter diff --git a/packages/stream_chat_persistence/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index 9b0dfef7..b2ac03f2 100644 --- a/packages/stream_chat_persistence/pubspec.yaml +++ b/packages/stream_chat_persistence/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_persistence homepage: https://github.com/GetStream/stream-chat-flutter description: Official Stream Chat Persistence library. Build your own chat experience using Dart and Flutter. -version: 4.3.0 +version: 4.4.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -19,7 +19,7 @@ dependencies: path: ^1.8.0 path_provider: ^2.0.1 sqlite3_flutter_libs: ^0.5.0 - stream_chat: ^4.4.0 + stream_chat: ^4.4.1 dev_dependencies: build_runner: ^2.0.1 dart_code_metrics: ^4.4.0 From 46e7c0b332671287775a154f3f19e42444f55201 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 28 Jul 2022 11:34:16 +0200 Subject: [PATCH 23/49] fix(ui): do not linkify system message mentions --- packages/stream_chat_flutter/lib/src/system_message.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/system_message.dart b/packages/stream_chat_flutter/lib/src/system_message.dart index 6135afc0..fa4033da 100644 --- a/packages/stream_chat_flutter/lib/src/system_message.dart +++ b/packages/stream_chat_flutter/lib/src/system_message.dart @@ -28,7 +28,7 @@ class StreamSystemMessage extends StatelessWidget { @override Widget build(BuildContext context) { final theme = StreamChatTheme.of(context); - final message = this.message.replaceMentions(); + final message = this.message.replaceMentions(linkify: false); final messageText = message.text; if (messageText == null) return const SizedBox.shrink(); From 4f696ab7c71e09b00cc347e944ba0ef0e3fda6b0 Mon Sep 17 00:00:00 2001 From: Gordon Date: Tue, 2 Aug 2022 10:43:11 +0200 Subject: [PATCH 24/49] docs: add guide to initialize stream chat (#1290) --- .../initialize_stream_chat_widget_tree.mdx | 696 ++++++++++++++++++ 1 file changed, 696 insertions(+) create mode 100644 docusaurus/docs/Flutter/guides/initialize_stream_chat_widget_tree.mdx diff --git a/docusaurus/docs/Flutter/guides/initialize_stream_chat_widget_tree.mdx b/docusaurus/docs/Flutter/guides/initialize_stream_chat_widget_tree.mdx new file mode 100644 index 00000000..13ad048d --- /dev/null +++ b/docusaurus/docs/Flutter/guides/initialize_stream_chat_widget_tree.mdx @@ -0,0 +1,696 @@ +--- +id: initialize_stream_chat_widget_tree +sidebar_position: 15 +title: Initialize Stream Chat in Part of the Widget Tree +--- + +If you’re creating a full-scale chat application, you probably want to have Stream Chat Flutter initialized at the top of your widget tree and the Stream user connected as soon as they open the application. + +However, if you only need chat functionality in a part of your application, then it’ll be better to delay Stream Chat initialization to when it’s needed. +This guide demonstrates three alternative ways for you to initialize Stream Chat Flutter for a part of your widget tree and to only connect a user when needed. + +## What To Keep In Mind? + +Before investigating potential solutions, let’s first take a look at the relevant Stream Chat widgets and classes. + +Most of the Stream Chat Flutter UI widgets rely on having a [StreamChat](https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter/stream_chat_and_theming/) ancestor in the widget tree. +The **StreamChat** widget is an [InheritedWidget](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html) that exposes the **StreamChatClient** through **BuildContext**. +This widget also initializes the [StreamChatCore](../stream_chat_flutter_core/stream_chat_core.mdx) widget and the **StreamChatTheme**. + +**StreamChatCore** is a **StatefulWidget** used to react to life cycle changes and system updates. +When the app goes into the background, the WebSocket connection is closed. +Conversely, a new connection is initiated when the app is back in the foreground. + +What is important to take note of is that **a connection is only established if a user is connected**. + +This means that if you have not yet called `client.connectUser(user, token)`, no connection will be made, and only background listeners will be registered to determine the app's foreground state. + +## Option 1: Builder and Connect/Disconnect User + +This option requires you to wrap your whole application with the **StreamChat** widget and to call `connectUser` and `disconnectUser` as needed. + +This option is the easiest, however, it requires **StreamChat** to be at the top of each route and as a result, will have a slight overhead as it’ll create the above-mentioned Stream widgets that may not yet be needed. + +### Exposing the StreamChat Widget + +First, you must expose the client and base Stream Chat widgets to the whole application. + +```dart +void main() { + final client = StreamChatClient( + 'q29npdvqjr99', + logLevel: Level.OFF, + ); + + runApp(MyApp(client: client)); +} + +class MyApp extends StatelessWidget { + const MyApp({ + Key? key, + required this.client, + }) : super(key: key); + + final StreamChatClient client; + + @override + Widget build(BuildContext context) { + return MaterialApp( + builder: (context, child) { + return StreamChat(client: client, child: child); + }, + home: const HomeScreen(), + ); + } +} +``` + +In the above code, you: + +1. Create a **StreamChatClient** instance +2. Pass the instance to the **StreamChat** widget +3. Expose **StreamChat** to the whole application within the **MaterialApp** `builder` + +A few important things to note: + +- The `builder` wraps the **StreamChat** widget for every route of our application. No matter where you are in the widget tree, you’ll be able to call `StreamChat.of(context)`. +- The state will only be created once for our application, as **StreamChat** is a **StatefulWidget** and the position of **StreamChat** in the widget tree remains the same throughout the application lifecycle. +- No connection will be made until you call `connectUser`. + +### Connecting and Disconnecting Users + +In **MaterialApp** above, the home page is set to **HomeScreen**. This screen could look something like the following: + +```dart +class HomeScreen extends StatelessWidget { + const HomeScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(), + body: const Center( + child: Text('Home Screen'), + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => const ChatSetup())); + }, + child: const Icon( + Icons.message, + ), + ), + ); + } +} +``` + +This screen shows a floating action button that on click navigates the user to the **ChatSetup**. We want to connect and disconnect a Stream user only when they go to the chat setup screen. + +```dart +class ChatSetup extends StatefulWidget { + const ChatSetup({ + super.key, + }); + + @override + State createState() => _ChatSetupState(); +} + +class _ChatSetupState extends State { + late final Future connectionFuture; + late final client = StreamChat.of(context).client; + + @override + void initState() { + super.initState(); + connectionFuture = client.connectUser( + User(id: 'USER_ID'), + 'TOKEN', + ); + } + + @override + void dispose() { + client.disconnectUser(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(), + body: FutureBuilder( + future: connectionFuture, + builder: (BuildContext context, AsyncSnapshot snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.waiting: + return const Center( + child: CircularProgressIndicator(), + ); + default: + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } else { + return const ChannelListPage(); + } + } + }, + ), + ); + } +} +``` + +Within `initState` you call `connectUser`; once the future for `connectUser` has completed a connection to the Stream API is established, you can then display the relevant Stream Chat UI widgets. + +Once the **ChatScreen** widget is disposed of, then `disconnectUser` will be called within the `dispose` method. + +### Caution + +In this example, `disconnectUser` will **only** be called when the **ChatSetup** widget is disposed. + +You need to ensure that this widget (route) is completely disposed of, or you need to call `disconnectUser` and `connectUser` manually when navigating to relevant parts of your application. + +For example, let’s say you have a button within one of the chat screens to navigate to a completely different part of your app, then you want to make sure the **ChatScreen** route is disposed of by forcing it to be removed: + +```dart +Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: ((context) => const HomeScreen()), + ), + (Route route) => false, +); +``` + +Or let’s say you wanted to pop back to the first route: + +```dart +Navigator.of(context).popUntil((route) => route.isFirst); +``` + +Both of these will ensure the route is disposed of and the user is disconnected as a result. + +## Option 2: A Nested Navigator + +Another approach would be to introduce a new [Navigator](https://api.flutter.dev/flutter/widgets/Navigator-class.html). +This has the benefit that everything related to Stream chat is contained to a specific part of the widget tree. + +### Defining Routes and Subroutes + +In this example, our application has the following routes. + +```dart +const routeHome = '/'; +const routePrefixChat = '/chat/'; +const routeChatHome = '$routePrefixChat$routeChatChannels'; +const routeChatChannels = 'chat_channels'; +const routeChatChannel = 'chat_channel'; +``` + +For the `/chat/` subroutes (**routePrefixChat**), this approach initializes Stream Chat in our application and introduces a nested navigator. + +Let’s explore the code: + +```dart +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({ + Key? key, + }) : super(key: key); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + final GlobalKey navigatorKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return Provider.value( + value: navigatorKey, + child: MaterialApp( + navigatorKey: navigatorKey, + initialRoute: routeHome, + onGenerateRoute: (settings) { + late Widget page; + if (settings.name == routeHome) { + page = const HomeScreen(); + } else if (settings.name!.startsWith(routePrefixChat)) { + final subRoute = settings.name!.substring(routePrefixChat.length); + page = ChatSetup( + setupChatRoute: subRoute, + ); + } else { + throw Exception('Unknown route: ${settings.name}'); + } + + return MaterialPageRoute( + builder: (context) { + return page; + }, + settings: settings, + ); + }, + ), + ); + } +} +``` + +In the above code you’re: + +1. Creating a navigator key, passing it to `MaterialApp`, and exposing it to the whole application using Provider (you can expose it however you want). +2. Creating `onGenerateRoute` that specifies what page to show depending on the route. Most importantly, if the route contains the **routePrefixChat,** it navigates to the **ChatSetup** page and passes in the remainder of the route. + +For example, `Navigator.pushNamed(context, routeChatHome)` will navigate to the **ChatSetup** page and pass in the subroute **routeChatChannels.** + +### Stream Chat Initialization, User Connection, and Nested Navigation + +Within the **ChatSetup** widget, we’ll initialize Stream chat, connect a user, and create a new Navigator that handles the sub-navigation for the chat-specific pages. + +```dart +class ChatSetup extends StatefulWidget { + const ChatSetup({Key? key, required this.setupChatRoute}) : super(key: key); + + final String setupChatRoute; + + @override + State createState() => _ChatSetupState(); +} + +class _ChatSetupState extends State { + late final Future connectionFuture; + late final client = StreamChatClient( + 'KEY', + logLevel: Level.OFF, + ); + + @override + void initState() { + super.initState(); + connectionFuture = client.connectUser( + User(id: 'USER_ID'), + 'TOKEN', + ); + } + + @override + void dispose() { + client.disconnectUser(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Material( + child: StreamChat( + client: client, + child: FutureBuilder( + future: connectionFuture, + builder: (BuildContext context, AsyncSnapshot snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.waiting: + return const Center( + child: CircularProgressIndicator(), + ); + default: + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } else { + return Navigator( + initialRoute: widget.setupChatRoute, + onGenerateRoute: _onGenerateRoute, + ); + } + } + }, + ), + ), + ); + } + + Route _onGenerateRoute(RouteSettings settings) { + late Widget page; + switch (settings.name) { + case routeChatChannels: + page = ChannelListPage( + client: client, + ); + break; + case routeChatChannel: + final channel = settings.arguments as Channel; + page = StreamChannel( + channel: channel, + child: const ChannelPage(), + ); + break; + default: + throw Exception('Unknown route: ${settings.name}'); + } + return MaterialPageRoute( + builder: (context) { + return page; + }, + settings: settings, + ); + } +} +``` + +This **ChatSetup** widget is similar to what it was in the first option, the only difference is that we’re also introducing a nested navigator and handling those nested routes. + +The steps for the above code are: + +1. Create a **StreamChatClient** instance +2. Call `connectUser` within `initState` and await the result using a **FutureBuilder** +3. Introduce a **StreamChat** widget into the widget tree +4. Introduce a new **Navigator** for the chat-specific routes +5. Call `disconnectUser` within `dispose` + +### Displaying Stream Chat UI Widgets and Global Navigation + +Then finally, the **ChannelListPage** could look something like the following: + +```dart +class ChannelListPage extends StatefulWidget { + const ChannelListPage({ + super.key, + required this.client, + }); + + final StreamChatClient client; + + @override + State createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + late final _controller = StreamChannelListController( + client: widget.client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + ); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _popChatPages() { + final nav = context.read>(); + nav.currentState!.pop(); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + _popChatPages(); + return true; + }, + child: Scaffold( + appBar: AppBar( + leading: BackButton( + onPressed: () { + _popChatPages(); + }, + ), + ), + body: RefreshIndicator( + onRefresh: _controller.refresh, + child: StreamChannelListView( + controller: _controller, + onChannelTap: (channel) { + Navigator.pushNamed(context, routeChatChannel, + arguments: channel); + }, + ), + ), + ), + ); + } +} +``` + +There are two important things to note in the above code: + +- We’re accessing the global **NavigatorState** using Provider and calling `pop()` on the back press. This will ensure that this entire route is disposed of and that the Stream connection is closed. +- Within `onChannelTap` we’re calling `pushNamed` and passing in the route to display a single channel page. This uses the navigator introduced in **ChatSetup**, which is the closest navigator within the widget tree. + +A few things to take note of: + +- You’ll always need to access the global navigator if you want to dispose of this route. you need to ensure that this route is popped or replaced, otherwise, the Stream connection will remain active for as long as the chat route is on the stack (or manually disconnected). + +## Option 3: Navigator 2.0 - Using GoRouter + +This final example will be a combination of the first two options. The following code is an example of how to initialize Stream Chat in a part of the widget tree using the [GoRouter package](https://pub.dev/packages/go_router). This solution will change depending on how you do routing in your Flutter application and which package (if any) you use. + +### Application Routes and Conditional Stream Initialization + +For this example we have the following routes and subroutes: + +```dart +|_ '/' -> home page + |_ 'settings/' - settings page + |_ 'chat/' - chat home, shows the channels list page + |_ 'channel/' - specific channel page +``` + +In the **MyApp** widget, we’ll initialize **GoRouter** and the **StreamChatClient**. +However, we will only expose the **StreamChat** widget for certain routes. +Additionally, we’ll create a **ChatSetup** widget that connects and disconnects the user. +This widget will also **only** be injected for the `/chat` routes. + +```dart +class MyApp extends StatefulWidget { + const MyApp({ + Key? key, + }) : super(key: key); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + final _client = StreamChatClient( + 'q29npdvqjr99', + logLevel: Level.OFF, + ); + bool wasPreviousRouteChat = false; + + late final _router = GoRouter( + initialLocation: '/', + navigatorBuilder: (context, state, child) { + if (state.location.startsWith('/chat')) { + wasPreviousRouteChat = true; + return StreamChat( + client: _client, + child: ChatSetup(client: _client, child: child), + ); + } else { + if (wasPreviousRouteChat) { + wasPreviousRouteChat = false; + return StreamChat( + client: _client, + child: child, + ); + } else { + return child; + } + } + }, + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const HomeScreen(), + routes: [ + GoRoute( + path: 'settings', + builder: (context, state) => const SettingsPage(), + ), + GoRoute( + path: 'chat', + builder: (context, state) => const ChannelListPage(), + routes: [ + GoRoute( + path: 'channel', + builder: (context, state) { + final channel = state.extra as Channel; + return StreamChannel( + channel: channel, + child: const ChannelPage(), + ); + }, + ), + ], + ), + ], + ), + ], + ); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + routeInformationParser: _router.routeInformationParser, + routeInformationProvider: _router.routeInformationProvider, + routerDelegate: _router.routerDelegate, + ); + } +} +``` + +If you’re unfamiliar with GoRouter it will help to first read the documentation and then come back to this guide. + +There are a few important things to note in the above code: + +- Define our routes and subroutes +- Specify the initial route with `initialLocation` +- Use the `navigatorBuilder` to wrap certain routes with **StreamChat** and **ChatSetup**. + - The `wasPreviousRouteChat` \***\*boolean is used to determine if the previous route was a chat route. This is important because when you press the back button from the `/chat` route and navigate to the `/` route, the **StreamChat** widget still needs to be accessible while the navigation transition occurs. However, if you then navigate to the `/setting` route, you no longer need the **StreamChat\*\* widget and can safely remove it. + +### Navigating With GoRouter + +Within the **HomeScreen** you can create a button that on press navigates to the `/chat` route: + +```dart +GoRouter.of(context).go('/chat'); +``` + +### Connecting and Disconnecting Users + +Same as before, we’ll use the **ChatSetup** widget to connect and disconnect a user. +This time the widget also takes in a **child** widget to display once the connection is finished. +The child widget is dependent on the route we’re navigating to. + +```dart +class ChatSetup extends StatefulWidget { + const ChatSetup({ + Key? key, + required this.client, + required this.child, + }) : super(key: key); + + final StreamChatClient client; + final Widget child; + + @override + State createState() => _ChatSetupState(); +} + +class _ChatSetupState extends State { + late final Future connectionFuture; + + @override + void initState() { + super.initState(); + connectionFuture = widget.client.connectUser( + User(id: 'USER_ID'), + 'TOKEN', + ); + } + + @override + void dispose() { + widget.client.disconnectUser(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Material( + child: FutureBuilder( + future: connectionFuture, + builder: (BuildContext context, AsyncSnapshot snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.waiting: + return const Center( + child: CircularProgressIndicator(), + ); + default: + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } else { + return widget.child; + } + } + }, + ), + ); + } +} +``` + +This will connect the Stream chat user within `initState` and disconnect the user on `dispose`. This is similar to the previous examples we explored. + +### Displaying the Stream UI Widgets + +The **ChannelListPage** can look something like the following: + +```dart +class ChannelListPage extends StatefulWidget { + const ChannelListPage({ + super.key, + }); + + @override + State createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + late final client = StreamChat.of(context).client; + late final _controller = StreamChannelListController( + client: client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + ); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(), + body: RefreshIndicator( + onRefresh: _controller.refresh, + child: StreamChannelListView( + controller: _controller, + onChannelTap: (channel) { + GoRouter.of(context).go('/chat/channel', extra: channel); + }, + ), + ), + ); + } +} +``` + +This is the same as before, the only difference is that the navigation is slightly different; now we’re navigating to the `/chat/channel` route for individual channel pages. + +## Conclusion + +This guide demonstrated three different ways to initialize Stream Chat in a part of the Flutter widget tree. + +There are two key takeaways + +1. Accessing **StreamChat** depends on your location in the widget tree +2. How you ultimately decide to expose **StreamChat** and connect users will be up to your application architecture and how you manage routing. + +The above are only examples that can be refined and tweaked to suit your needs. From b3a7ca2897315b6c9d25c653fb6cc8d40c9a56af Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 4 Aug 2022 16:44:57 +0530 Subject: [PATCH 25/49] fix(llc): Fix `Channel.removeMessage` not able to remove thread message. (#1294) Signed-off-by: xsahil03x --- packages/stream_chat/CHANGELOG.md | 6 ++++++ packages/stream_chat/lib/src/client/channel.dart | 12 +++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index e8a7aa12..4a064d87 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +🐞 Fixed + +- Fix `Channel.removeMessage` not able to remove thread message. + ## 4.4.1 🐞 Fixed diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 64c20d6b..d610ddd5 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -1955,11 +1955,13 @@ class ChannelClientState { // Early return in case the thread is not available if (!newThreads.containsKey(parentId)) return; - _threads = newThreads - ..update( - parentId, - (messages) => messages..removeWhere((e) => e.id == message.id), - ); + // Remove thread message shown in thread page. + newThreads.update( + parentId, + (messages) => [...messages.where((e) => e.id != message.id)], + ); + + _threads = newThreads; // Early return if the thread message is not shown in channel. if (message.showInChannel == false) return; From bd1876c1596dc1c779ec6f8ae3eee2b86af6baeb Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 5 Aug 2022 20:27:32 +0530 Subject: [PATCH 26/49] fix(ui): fix `stopTyping` unhandled exceptions when network is off or spotty. (#1296) * feat(llc, ui): Introduce `keyStrokeHandler` to properly handle keyStrokes. Signed-off-by: xsahil03x * chore(ui): update CHANGELOG.md Signed-off-by: xsahil03x * test(llc): add key_stroke_handler_test.dart Signed-off-by: xsahil03x --- .../stream_chat/lib/src/client/channel.dart | 260 +++++++++--------- .../lib/src/client/key_stroke_handler.dart | 114 ++++++++ packages/stream_chat/lib/stream_chat.dart | 1 + .../test/src/client/channel_test.dart | 21 +- .../src/client/key_stroke_handler_test.dart | 73 +++++ packages/stream_chat_flutter/CHANGELOG.md | 7 + .../lib/src/message_input.dart | 11 +- .../message_input/stream_message_input.dart | 14 +- 8 files changed, 352 insertions(+), 149 deletions(-) create mode 100644 packages/stream_chat/lib/src/client/key_stroke_handler.dart create mode 100644 packages/stream_chat/test/src/client/key_stroke_handler_test.dart diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index d610ddd5..7565b02b 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -8,6 +8,10 @@ import 'package:stream_chat/src/client/retry_queue.dart'; import 'package:stream_chat/src/core/util/utils.dart'; import 'package:stream_chat/stream_chat.dart'; +/// The maximum time the incoming [Event.typingStart] event is valid before a +/// [Event.typingStop] event is emitted automatically. +const incomingTypingStartEventTimeout = 7; + /// Class that manages a specific channel. /// /// #### Channel name @@ -1493,36 +1497,38 @@ class Channel { ) .where((e) => e.cid == cid); - DateTime? _lastTypingEvent; + late final _keyStrokeHandler = KeyStrokeHandler( + onStartTyping: startTyping, + onStopTyping: stopTyping, + ); - /// First of the [EventType.typingStart] and [EventType.typingStop] events - /// based on the users keystrokes. Call this on every keystroke. + /// Sends the [Event.typingStart] event and schedules a timer to invoke the + /// [Event.typingStop] event. + /// + /// This is meant to be called every time the user presses a key. Future keyStroke([String? parentId]) async { - if (config?.typingEvents == false) { - return; - } + if (config?.typingEvents == false) return; - client.logger.info('start typing'); - final now = DateTime.now(); - - if (_lastTypingEvent == null || - now.difference(_lastTypingEvent!).inSeconds >= 2) { - _lastTypingEvent = now; - await sendEvent(Event( - type: EventType.typingStart, - parentId: parentId, - )); - } + client.logger.info('KeyStroke received'); + return _keyStrokeHandler(parentId); } - /// Sets last typing to null and sends the typing.stop event. + /// Sends the [EventType.typingStart] event. + Future startTyping([String? parentId]) async { + if (config?.typingEvents == false) return; + + client.logger.info('start typing'); + await sendEvent(Event( + type: EventType.typingStart, + parentId: parentId, + )); + } + + /// Sends the [EventType.typingStop] event. Future stopTyping([String? parentId]) async { - if (config?.typingEvents == false) { - return; - } + if (config?.typingEvents == false) return; client.logger.info('stop typing'); - _lastTypingEvent = null; await sendEvent(Event( type: EventType.typingStop, parentId: parentId, @@ -1532,6 +1538,7 @@ class Channel { /// Call this method to dispose the channel client. void dispose() { state?.dispose(); + _keyStrokeHandler.cancel(); } void _checkInitialized() { @@ -1593,9 +1600,9 @@ class ChannelClientState { _listenMemberUnbanned(); - _startCleaning(); + _startCleaningStaleTypingEvents(); - _startCleaningPinnedMessages(); + _startCleaningStalePinnedMessages(); _channel._client.chatPersistenceClient ?.getChannelThreads(_channel.cid!) @@ -1613,7 +1620,8 @@ class ChannelClientState { }); } - final _subscriptions = []; + final Channel _channel; + final _subscriptions = CompositeSubscription(); void _checkExpiredAttachmentMessages(ChannelState channelState) async { final expiredAttachmentMessagesId = channelState.messages @@ -1786,20 +1794,15 @@ class ChannelClientState { /// Retry failed message. Future retryFailedMessages() async { - final failedMessages = - [...messages, ...threads.values.expand((v) => v)] - .where( - (message) => - message.status != MessageSendingStatus.sent && - message.createdAt.isBefore( - DateTime.now().subtract( - const Duration( - seconds: 5, - ), - ), - ), - ) - .toList(); + final failedMessages = [...messages, ...threads.values.expand((v) => v)] + .where( + (message) => + message.status != MessageSendingStatus.sent && + message.createdAt.isBefore( + DateTime.now().subtract(const Duration(seconds: 5)), + ), + ) + .toList(); _retryQueue.add(failedMessages); } @@ -1986,12 +1989,7 @@ class ChannelClientState { } _subscriptions.add( - _channel - .on( - EventType.messageRead, - EventType.notificationMarkRead, - ) - .listen( + _channel.on(EventType.messageRead, EventType.notificationMarkRead).listen( (event) { final readList = List.from(_channelState.read ?? []); final userReadIndex = @@ -2252,34 +2250,29 @@ class ChannelClientState { ); } - /// Channel related typing users last value. - Map get typingEvents => _typingEventsController.value; - /// Channel related typing users stream. Stream> get typingEventsStream => _typingEventsController.stream; - final BehaviorSubject> _typingEventsController = - BehaviorSubject.seeded({}); - - final Channel _channel; - final Map _typings = {}; + /// Channel related typing users last value. + Map get typingEvents => _typingEventsController.value; + final _typingEventsController = BehaviorSubject.seeded({}); void _listenTypingEvents() { - if (_channelState.channel?.config.typingEvents == false) { - return; - } + if (_channelState.channel?.config.typingEvents == false) return; + + final currentUser = _channel.client.state.currentUser; + if (currentUser == null) return; _subscriptions ..add( _channel.on(EventType.typingStart).listen( (event) { - if (event.user != null) { - final user = event.user!; - if (user.id != _channel.client.state.currentUser?.id) { - _typings[user] = event; - _typingEventsController.add(_typings); - } + final user = event.user; + if (user != null && user.id != currentUser.id) { + final events = {...typingEvents}; + events[user] = event; + _typingEventsController.add(events); } }, ), @@ -2287,112 +2280,109 @@ class ChannelClientState { ..add( _channel.on(EventType.typingStop).listen( (event) { - if (event.user != null) { - final user = event.user!; - if (user.id != _channel.client.state.currentUser?.id) { - _typings.remove(event.user); - _typingEventsController.add(_typings); - } + final user = event.user; + if (user != null && user.id != currentUser.id) { + final events = {...typingEvents}..remove(user); + _typingEventsController.add(events); } }, ), ) ..add( - _channel - .on() - .where((event) => - event.user != null && - members.any((m) => m.userId == event.user!.id)) - .listen( + _channel.on().where((event) { + final user = event.user; + if (user == null) return false; + return members.any((m) => m.userId == user.id); + }).listen( (event) { final newMembers = List.from(members); final oldMemberIndex = newMembers.indexWhere((m) => m.userId == event.user!.id); if (oldMemberIndex > -1) { final oldMember = newMembers.removeAt(oldMemberIndex); - updateChannelState(ChannelState( - members: [ - ...newMembers, - oldMember.copyWith( - user: event.user, - ), - ], - )); + updateChannelState( + ChannelState( + members: [ + ...newMembers, + oldMember.copyWith( + user: event.user, + ), + ], + ), + ); } }, ), ); } - Timer? _cleaningTimer; + Timer? _staleTypingEventsCleanerTimer; - void _startCleaning() { - if (_channelState.channel?.config.typingEvents == false) { - return; - } + // Checks and removes stale typing events that were not explicitly stopped by + // the sender due to technical difficulties. e.g. process death, loss of + // Internet connection or custom implementation. + void _startCleaningStaleTypingEvents() { + if (_channelState.channel?.config.typingEvents == false) return; - _cleaningTimer = Timer.periodic(const Duration(seconds: 1), (_) { - final now = DateTime.now(); - - if (_channel._lastTypingEvent != null && - now.difference(_channel._lastTypingEvent!).inSeconds > 1) { - _channel.stopTyping(); - } - - _clean(); - }); + _staleTypingEventsCleanerTimer = Timer.periodic( + const Duration(seconds: 1), + (_) { + final now = DateTime.now(); + typingEvents.forEach((user, event) { + if (now.difference(event.createdAt).inSeconds > + incomingTypingStartEventTimeout) { + _channel.client.handleEvent( + Event( + type: EventType.typingStop, + user: user, + cid: _channel.cid, + parentId: event.parentId, + ), + ); + } + }); + }, + ); } - late Timer _pinnedMessagesTimer; + Timer? _stalePinnedMessagesCleanerTimer; - void _startCleaningPinnedMessages() { - _pinnedMessagesTimer = Timer.periodic(const Duration(seconds: 30), (_) { - final now = DateTime.now(); - var expiredMessages = channelState.pinnedMessages - ?.where((m) => m.pinExpires?.isBefore(now) == true) - .toList(); - if (expiredMessages != null && expiredMessages.isNotEmpty) { - expiredMessages = expiredMessages - .map((m) => m.copyWith( - pinExpires: null, - pinned: false, - )) + // Checks and removes stale pinned messages that are not valid anymore. + void _startCleaningStalePinnedMessages() { + _stalePinnedMessagesCleanerTimer = Timer.periodic( + const Duration(seconds: 30), + (_) { + final now = DateTime.now(); + var expiredMessages = channelState.pinnedMessages + ?.where((m) => m.pinExpires?.isBefore(now) == true) .toList(); + if (expiredMessages != null && expiredMessages.isNotEmpty) { + expiredMessages = expiredMessages + .map((m) => m.copyWith( + pinExpires: null, + pinned: false, + )) + .toList(); - updateChannelState(_channelState.copyWith( - pinnedMessages: pinnedMessages.where(_pinIsValid).toList(), - messages: expiredMessages, - )); - } - }); - } - - void _clean() { - final now = DateTime.now(); - _typings.forEach((user, event) { - if (now.difference(event.createdAt).inSeconds > 7) { - _channel.client.handleEvent( - Event( - type: EventType.typingStop, - user: user, - cid: _channel.cid, - parentId: event.parentId, - ), - ); - } - }); + updateChannelState(_channelState.copyWith( + pinnedMessages: pinnedMessages.where(_pinIsValid).toList(), + messages: expiredMessages, + )); + } + }, + ); } /// Call this method to dispose this object. void dispose() { _debouncedUpdatePersistenceChannelState.cancel(); _retryQueue.dispose(); - _subscriptions.forEach((s) => s.cancel()); + _subscriptions.cancel(); _channelStateController.close(); _isUpToDateController.close(); _threadsController.close(); - _cleaningTimer?.cancel(); - _pinnedMessagesTimer.cancel(); + _staleTypingEventsCleanerTimer?.cancel(); + _stalePinnedMessagesCleanerTimer?.cancel(); _typingEventsController.close(); } } diff --git a/packages/stream_chat/lib/src/client/key_stroke_handler.dart b/packages/stream_chat/lib/src/client/key_stroke_handler.dart new file mode 100644 index 00000000..f67017b9 --- /dev/null +++ b/packages/stream_chat/lib/src/client/key_stroke_handler.dart @@ -0,0 +1,114 @@ +import 'dart:async'; + +/// A class that manages buffering typing events and call [onTypingStarted] and +/// [onTypingStopped] accordingly in a timed manner. +/// +/// This class is used by [Channel] to manage typing events. +class KeyStrokeHandler { + /// Creates a new instance of [KeyStrokeHandler]. + KeyStrokeHandler({ + this.startTypingEventTimeout = 1, + this.startTypingResendInterval = 3, + required this.onStartTyping, + required this.onStopTyping, + }); + + /// The number of seconds from the last [onStartTyping] callback until + /// the [onStopTyping] callback is automatically invoked. + final int startTypingEventTimeout; + + /// The number of seconds after the last [onStartTyping] callback before + /// the [onStartTyping] callback is automatically invoked again. + final int startTypingResendInterval; + + /// Called when a `typingStart` event needs to be send. + final Future Function([String? parentId]) onStartTyping; + + /// Called when a `typingStop` event needs to be send. + final Future Function([String? parentId]) onStopTyping; + + Timer? _keyStrokeTimer; + String? _currentParentId; + DateTime? _lastTypingEvent; + Completer? _keyStrokeCompleter; + + Future _startTyping(String? parentId) { + _currentParentId = parentId; + _lastTypingEvent = DateTime.now(); + return onStartTyping(parentId); + } + + Future _stopTyping(String? parentId) { + _currentParentId = null; + _lastTypingEvent = null; + return onStopTyping(parentId); + } + + // Completes the key stroke completer if it is not yet completed. + void _completeKeyStrokeCompleterIfRequired() { + final completer = _keyStrokeCompleter; + if (completer != null && !completer.isCompleted) completer.complete(); + } + + // Completes the completer if available and not yet completed then creates a + // new completer and returns it. + Completer _resetKeyStrokeCompleter() { + _completeKeyStrokeCompleterIfRequired(); + return _keyStrokeCompleter = Completer(); + } + + // Cancels the key stroke timer if it is running. + void _cancelKeyStrokeTimer() { + _keyStrokeTimer?.cancel(); + _keyStrokeTimer = null; + } + + /// Cancels the handler and stops the typing event. + void cancel() { + // If the user is typing, stop typing. + // This is needed to prevent the user from being stuck in typing mode. + if (_lastTypingEvent != null) { + // We don't need to handle the error here + _stopTyping(_currentParentId).catchError((_) {}); + } + _cancelKeyStrokeTimer(); + _completeKeyStrokeCompleterIfRequired(); + } + + /// Invokes the [onStartTyping] callback and schedules a timer to invoke the + /// [onStopTyping] callback. + /// + /// This is meant to be called every time the user presses a key. The method + /// will manage requests and timer as needed. + Future call([String? parentId]) async { + final completer = _resetKeyStrokeCompleter(); + + _cancelKeyStrokeTimer(); + + _keyStrokeTimer = Timer(Duration(seconds: startTypingEventTimeout), () { + _stopTyping(parentId).then((_) { + if (completer.isCompleted) return; + completer.complete(); + }).onError((error, stackTrace) { + if (completer.isCompleted) return; + completer.completeError(error!, stackTrace); + }); + }); + + // If the user is typing too long, it should call [onStartTyping] again. + final now = DateTime.now(); + final lastTypingEvent = _lastTypingEvent; + if (lastTypingEvent == null || + now.difference(lastTypingEvent).inMilliseconds > + // startTypingResendInterval in milliseconds + startTypingResendInterval * 1000) { + _startTyping(parentId).onError((error, stackTrace) { + _cancelKeyStrokeTimer(); + if (completer.isCompleted) return; + completer.completeError(error!, stackTrace); + }); + } + + return completer.future; + } +} diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 2755224f..7f4c22ef 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -38,6 +38,7 @@ export './src/permission_type.dart'; export './src/ws/connection_status.dart'; export 'src/client/channel.dart'; export 'src/client/client.dart'; +export 'src/client/key_stroke_handler.dart'; export 'src/core/api/attachment_file_uploader.dart' show AttachmentFileUploader; export 'src/core/api/requests.dart'; export 'src/core/api/responses.dart'; diff --git a/packages/stream_chat/test/src/client/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart index f90430f2..cf2fc042 100644 --- a/packages/stream_chat/test/src/client/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel_test.dart @@ -2658,16 +2658,22 @@ void main() { }); test( - '''should send `typingStart` event if there is not already a typingEvent or the difference between the two is >= 2 seconds''', + '''should send `typingStart` event if there is not already a typingEvent or the difference between the two is > 3 seconds''', () async { - final typingEvent = Event(type: EventType.typingStart); + final startTypingEvent = Event(type: EventType.typingStart); + final stopTypingEvent = Event(type: EventType.typingStop); when(() => channel.config?.typingEvents).thenReturn(true); when(() => client.sendEvent( channelId, channelType, - any(that: isSameEventAs(typingEvent)), + any(that: isSameEventAs(startTypingEvent)), + )).thenAnswer((_) async => EmptyResponse()); + when(() => client.sendEvent( + channelId, + channelType, + any(that: isSameEventAs(stopTypingEvent)), )).thenAnswer((_) async => EmptyResponse()); await channel.keyStroke(); @@ -2675,7 +2681,12 @@ void main() { verify(() => client.sendEvent( channelId, channelType, - any(that: isSameEventAs(typingEvent)), + any(that: isSameEventAs(startTypingEvent)), + )).called(1); + verify(() => client.sendEvent( + channelId, + channelType, + any(that: isSameEventAs(stopTypingEvent)), )).called(1); }, ); @@ -2688,7 +2699,7 @@ void main() { final typingStopEvent = Event(type: EventType.typingStop); - await channel.keyStroke(); + await channel.stopTyping(); verifyNever(() => client.sendEvent( channelId, diff --git a/packages/stream_chat/test/src/client/key_stroke_handler_test.dart b/packages/stream_chat/test/src/client/key_stroke_handler_test.dart new file mode 100644 index 00000000..d0a13352 --- /dev/null +++ b/packages/stream_chat/test/src/client/key_stroke_handler_test.dart @@ -0,0 +1,73 @@ +// ignore_for_file: avoid_redundant_argument_values + +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/test.dart'; + +mixin OnKeyStrokeEvent { + Future call([String? parentId]); +} + +class OnStartTyping extends Mock implements OnKeyStrokeEvent {} + +class OnStopTyping extends Mock implements OnKeyStrokeEvent {} + +void main() { + final onStartTyping = OnStartTyping(); + final onStopTyping = OnStopTyping(); + late KeyStrokeHandler keyStrokeHandler; + + const startTypingEventTimeout = 1; + const startTypingResendInterval = 2; + + setUp(() { + when(() => onStartTyping(any())).thenAnswer((_) => Future.value()); + when(() => onStopTyping(any())).thenAnswer((_) => Future.value()); + + keyStrokeHandler = KeyStrokeHandler( + onStartTyping: onStartTyping, + onStopTyping: onStopTyping, + startTypingEventTimeout: startTypingEventTimeout, + startTypingResendInterval: startTypingResendInterval, + ); + }); + + tearDown(() { + keyStrokeHandler.cancel(); + clearInteractions(onStartTyping); + clearInteractions(onStopTyping); + }); + + group('call', () { + test('should work fine', () { + expect(keyStrokeHandler.call(), completes); + }); + + test('should call onStartTyping', () async { + keyStrokeHandler.call(); + verify(() => onStartTyping(any())).called(1); + }); + + test('should call onStopTyping', () async { + keyStrokeHandler + ..call() + ..cancel(); + verify(() => onStopTyping(any())).called(1); + }); + + test('should call onStartTyping after startTypingResendInterval', () async { + final watch = Stopwatch()..start(); + while (watch.elapsed.inSeconds <= startTypingResendInterval) { + keyStrokeHandler.call(); + } + watch.stop(); + verify(() => onStartTyping(any())).called(2); + }); + + test('should call onStopTyping after startTypingEventTimeout', () async { + keyStrokeHandler.call(); + await Future.delayed(const Duration(seconds: startTypingEventTimeout)); + verify(() => onStopTyping(any())).called(1); + }); + }); +} diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index f098c65c..be7ffa2b 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,10 @@ +## Upcoming + +🐞 Fixed + +- [[#882]](https://github.com/GetStream/stream-chat-flutter/issues/882) Lots of unhandled exceptions + when network is off or spotty. + ## 4.4.1 🐞 Fixed diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 76031e5f..f58d7bad 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -824,9 +824,14 @@ class MessageInputState extends State { value = value.trim(); final channel = StreamChannel.of(context).channel; - if (value.isNotEmpty) { - // ignore: no-empty-block - channel.keyStroke(widget.parentMessage?.id).catchError((e) {}); + if (value.isNotEmpty && + channel.ownCapabilities.contains(PermissionType.sendTypingEvents)) { + // Notify the server that the user started typing. + channel.keyStroke(widget.parentMessage?.id).onError( + (error, stackTrace) { + widget.onError?.call(error!, stackTrace); + }, + ); } var actionsLength = widget.actions.length; diff --git a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart index 56ffcfe1..db1c1925 100644 --- a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart +++ b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart @@ -945,12 +945,14 @@ class StreamMessageInputState extends State value = value.trim(); final channel = StreamChannel.of(context).channel; - if (channel.ownCapabilities.contains(PermissionType.sendTypingEvents) && - value.isNotEmpty) { - channel - .keyStroke(_effectiveController.value.parentId) - // ignore: no-empty-block - .catchError((e) {}); + if (value.isNotEmpty && + channel.ownCapabilities.contains(PermissionType.sendTypingEvents)) { + // Notify the server that the user started typing. + channel.keyStroke(_effectiveController.message.parentId).onError( + (error, stackTrace) { + widget.onError?.call(error!, stackTrace); + }, + ); } var actionsLength = widget.actions.length; From 9fa5ac49d0e2a144c07b3ed1b4fdc355235092bd Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 8 Aug 2022 15:11:28 +0530 Subject: [PATCH 27/49] fix(core): only handle the channel list event if the state is success. Signed-off-by: xsahil03x --- .../stream_chat_flutter_core/CHANGELOG.md | 55 ++++++++++++------- .../lib/src/paged_value_notifier.dart | 3 + .../src/stream_channel_list_controller.dart | 3 + 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index ba44eb78..3d6aa989 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,8 @@ +## Upcoming + +- [#1269](https://github.com/GetStream/stream-chat-flutter/issues/1269) + Fix `ChannelListEventHandler` castError at PagedValue.asSuccess. + ## 4.4.1 - Updated `stream_chat` dependency to [`4.4.1`](https://pub.dev/packages/stream_chat/changelog). @@ -16,8 +21,10 @@ 🔄 Changed -- Deprecated `before` and `after` parameters in `StreamChannel.queryAroundMessage`. Use `limit` instead. -- Deprecated `before` and `after` parameters in `StreamChannel.loadChannelAtMessage`. Use `limit` instead. +- Deprecated `before` and `after` parameters in `StreamChannel.queryAroundMessage`. Use `limit` + instead. +- Deprecated `before` and `after` parameters in `StreamChannel.loadChannelAtMessage`. Use `limit` + instead. ## 4.1.0 @@ -30,14 +37,17 @@ ## 4.0.0 -For upgrading to V4, please refer to the [V4 Migration Guide](https://getstream.io/chat/docs/sdk/flutter/guides/migration_guide_4_0/) +For upgrading to V4, please refer to +the [V4 Migration Guide](https://getstream.io/chat/docs/sdk/flutter/guides/migration_guide_4_0/) - Deprecated `UsersBloc` in favor of `StreamUserListController` to control the user list. -- Deprecated `MessageSearchBloc` in favor of `StreamMessageSearchListController` to control the user list. +- Deprecated `MessageSearchBloc` in favor of `StreamMessageSearchListController` to control the user + list. ## 4.0.0-beta.2 -- Updated `stream_chat` dependency to [`4.0.0-beta.2`](https://pub.dev/packages/stream_chat/changelog). +- Updated `stream_chat` dependency + to [`4.0.0-beta.2`](https://pub.dev/packages/stream_chat/changelog). ## 4.0.0-beta.0 @@ -48,7 +58,8 @@ For upgrading to V4, please refer to the [V4 Migration Guide](https://getstream. - Deprecated `ChannelsBloc` in favor of `StreamChannelListController` to control the channel list. - Added `MessageTextFieldController` to be used with the new `StreamTextField` ui widget. -- Updated `stream_chat` dependency to [`4.0.0-beta.0`](https://pub.dev/packages/stream_chat/changelog). +- Updated `stream_chat` dependency + to [`4.0.0-beta.0`](https://pub.dev/packages/stream_chat/changelog). ## 3.6.1 @@ -67,12 +78,14 @@ For upgrading to V4, please refer to the [V4 Migration Guide](https://getstream. - Updated `stream_chat` dependency to [`3.5.0`](https://pub.dev/packages/stream_chat/changelog). ## 3.4.0 + - Updated `stream_chat` dependency to [`3.4.0`](https://pub.dev/packages/stream_chat/changelog). 🐞 Fixed - Do not move a channel to top if the new message is from a thread. -- [[#848]](https://github.com/GetStream/stream-chat-flutter/issues/848) Fixed "Bad state: Cannot add new events after calling close" by replacing all `.add` methods with a new `.safeAdd`. +- [[#848]](https://github.com/GetStream/stream-chat-flutter/issues/848) Fixed "Bad state: Cannot add + new events after calling close" by replacing all `.add` methods with a new `.safeAdd`. ## 3.3.1 @@ -133,8 +146,8 @@ For upgrading to V4, please refer to the [V4 Migration Guide](https://getstream. 🐞 Fixed - Fixed `MessageSearchBloc` pagination. -- [[#673]](https://github.com/GetStream/stream-chat-flutter/issues/673): Fix `Core Widgets` not getting rebuild with new - data on configuration change. +- [[#673]](https://github.com/GetStream/stream-chat-flutter/issues/673): Fix `Core Widgets` not + getting rebuild with new data on configuration change. ## 2.2.1 @@ -152,8 +165,8 @@ For upgrading to V4, please refer to the [V4 Migration Guide](https://getstream. 🐞 Fixed -- [#612](https://github.com/GetStream/stream-chat-flutter/issues/612) `ChannelListView` pagination doesn't work after - refresh +- [#612](https://github.com/GetStream/stream-chat-flutter/issues/612) `ChannelListView` pagination + doesn't work after refresh ## 2.1.1 @@ -171,20 +184,23 @@ For upgrading to V4, please refer to the [V4 Migration Guide](https://getstream. 🔄 Changed -- `StreamChatCore.of(context).user` is now deprecated in favor of `StreamChatCore.of(context).currentUser`. -- `StreamChatCore.of(context).userStream` is now deprecated in favor of `StreamChatCore.of(context).currentUserStream`. +- `StreamChatCore.of(context).user` is now deprecated in favor + of `StreamChatCore.of(context).currentUser`. +- `StreamChatCore.of(context).userStream` is now deprecated in favor + of `StreamChatCore.of(context).currentUserStream`. ## 2.0.0 🛑️ Breaking Changes from `1.5.3` - migrate this package to null safety -- `channelsBloc.queryChannels()`, `ChannelListCore` options param/property is removed in favor of individual - params/properties +- `channelsBloc.queryChannels()`, `ChannelListCore` options param/property is removed in favor of + individual params/properties - `options.state` -> bool state - `options.watch` -> bool watch - `options.presence` -> bool presence -- `usersBloc.queryUsers()`, `UserListCore` options param/property is removed in favor of individual params/properties +- `usersBloc.queryUsers()`, `UserListCore` options param/property is removed in favor of individual + params/properties - `options.presence` -> bool presence ✅ Added @@ -204,12 +220,13 @@ For upgrading to V4, please refer to the [V4 Migration Guide](https://getstream. 🛑️ Breaking Changes from `2.0.0-nullsafety.7` -- `channelsBloc.queryChannels()`, `ChannelListCore` options param/property is removed in favor of individual - params/properties +- `channelsBloc.queryChannels()`, `ChannelListCore` options param/property is removed in favor of + individual params/properties - `options.state` -> bool state - `options.watch` -> bool watch - `options.presence` -> bool presence -- `usersBloc.queryUsers()`, `UserListCore` options param/property is removed in favor of individual params/properties +- `usersBloc.queryUsers()`, `UserListCore` options param/property is removed in favor of individual + params/properties - `options.presence` -> bool presence ## 2.0.0-nullsafety.7 diff --git a/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.dart b/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.dart index 9525fea4..c122be1d 100644 --- a/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.dart +++ b/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.dart @@ -111,6 +111,9 @@ abstract class PagedValue with _$PagedValue { /// Returns `true` if the [PagedValue] is [Success]. bool get isSuccess => this is Success; + /// Returns `true` if the [PagedValue] is not [Success]. + bool get isNotSuccess => !isSuccess; + /// Returns the [PagedValue] as [Success]. Success get asSuccess { assert( diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart index 61193ac2..bea403be 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart @@ -223,6 +223,9 @@ class StreamChannelListController extends PagedValueNotifier { client.on().skip(1) // Skipping the last emitted event. // We only need to handle the latest events. .listen((event) { + // Only handle the event if the value is in success state. + if (value.isNotSuccess) return; + // Returns early if the event is already handled by the listener. if (eventListener?.call(event) ?? false) return; From 19244738ba9be2f4a443df495d8b89bf14da63eb Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 8 Aug 2022 15:50:09 +0530 Subject: [PATCH 28/49] feat(llc): add `hide_history` flag in `channel.addMembers`. Signed-off-by: xsahil03x --- packages/stream_chat/CHANGELOG.md | 4 ++++ packages/stream_chat/lib/src/client/channel.dart | 11 ++++++++++- packages/stream_chat/lib/src/client/client.dart | 2 ++ .../stream_chat/lib/src/core/api/channel_api.dart | 2 ++ .../test/src/core/api/channel_api_test.dart | 3 +++ 5 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 4a064d87..d04b7ec1 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -4,6 +4,10 @@ - Fix `Channel.removeMessage` not able to remove thread message. +✅ Added + +- Added `hide_history` flag in `client.addChannelMembers`, `channel.addMembers`. + ## 4.4.1 🐞 Fixed diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 7565b02b..688ce834 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -1151,9 +1151,18 @@ class Channel { Future addMembers( List memberIds, [ Message? message, + // TODO: Convert to optional parameters in v5.0.0 + // ignore: avoid_positional_boolean_parameters + bool hideHistory = false, ]) async { _checkInitialized(); - return _client.addChannelMembers(id!, type, memberIds, message: message); + return _client.addChannelMembers( + id!, + type, + memberIds, + message: message, + hideHistory: hideHistory, + ); } /// Invite members to the channel. diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index a8c4dd7b..c2950377 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -1044,12 +1044,14 @@ class StreamChatClient { String channelType, List memberIds, { Message? message, + bool hideHistory = false, }) => _chatApi.channel.addMembers( channelId, channelType, memberIds, message: message, + hideHistory: hideHistory, ); /// Remove members from the channel diff --git a/packages/stream_chat/lib/src/core/api/channel_api.dart b/packages/stream_chat/lib/src/core/api/channel_api.dart index 93d17870..08f6b6e4 100644 --- a/packages/stream_chat/lib/src/core/api/channel_api.dart +++ b/packages/stream_chat/lib/src/core/api/channel_api.dart @@ -210,12 +210,14 @@ class ChannelApi { String channelType, List memberIds, { Message? message, + bool hideHistory = false, }) async { final response = await _client.post( _getChannelUrl(channelId, channelType), data: { 'add_members': memberIds, 'message': message, + 'hide_history': hideHistory, }, ); return AddMembersResponse.fromJson(response.data); diff --git a/packages/stream_chat/test/src/core/api/channel_api_test.dart b/packages/stream_chat/test/src/core/api/channel_api_test.dart index bd0403de..4e2e4fc2 100644 --- a/packages/stream_chat/test/src/core/api/channel_api_test.dart +++ b/packages/stream_chat/test/src/core/api/channel_api_test.dart @@ -376,6 +376,7 @@ void main() { const memberIds = ['test-member-id-1', 'test-member-id-2']; final channelModel = ChannelModel(id: channelId, type: channelType); final message = Message(id: 'test-message-id', text: 'members-added'); + const hideHistory = true; final path = _getChannelUrl(channelId, channelType); @@ -384,6 +385,7 @@ void main() { data: { 'add_members': memberIds, 'message': message, + 'hide_history': hideHistory, }, )).thenAnswer((_) async => successResponse(path, data: { 'channel': channelModel.toJson(), @@ -395,6 +397,7 @@ void main() { channelType, memberIds, message: message, + hideHistory: hideHistory, ); expect(res, isNotNull); From 55bb828bd15f5128b37fec5e564e938bc73b0d62 Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Wed, 10 Aug 2022 14:36:39 +0200 Subject: [PATCH 29/49] perf: improve image resizing --- .../lib/src/attachment/file_attachment.dart | 1 + .../lib/src/attachment/image_attachment.dart | 16 +++++++---- .../stream_attachment_picker.dart | 28 ++++++++++++++++--- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart index 3d0c3314..be1b33bb 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart @@ -100,6 +100,7 @@ class StreamFileAttachment extends StreamAttachmentWidget { borderRadius: BorderRadius.circular(8), ); + // TODO: Improve image memory. This is using the full image instead of a smaller version (thumbnail) Widget _getFileTypeImage(BuildContext context) { if (isImageAttachment) { return Material( diff --git a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart index c022ee48..a5537140 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart @@ -71,16 +71,20 @@ class StreamImageAttachment extends StreamAttachmentWidget { var imageUri = Uri.parse(imageUrl); if (imageUri.host.endsWith('stream-io-cdn.com') && - imageUri.queryParameters['h'] == '*' && - imageUri.queryParameters['w'] == '*' && - imageUri.queryParameters['crop'] == '*' && - imageUri.queryParameters['resize'] == '*') { + (imageUri.queryParameters['h'] == null || + imageUri.queryParameters['h'] == '*') && + (imageUri.queryParameters['w'] == null || + imageUri.queryParameters['w'] == '*') && + (imageUri.queryParameters['crop'] == null || + imageUri.queryParameters['crop'] == '*') && + (imageUri.queryParameters['resize'] == null || + imageUri.queryParameters['resize'] == '*')) { imageUri = imageUri.replace(queryParameters: { ...imageUri.queryParameters, - 'h': '400', + 'h': '400', // TODO: Are these sizes optimal? Consider web/desktop 'w': '400', 'crop': 'center', - 'resize': 'crop', + 'resize': 'clip', }); } else if (imageUri.host.endsWith('stream-cloud-uploads.imgix.net')) { imageUri = imageUri.replace(queryParameters: { diff --git a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart index 2473cf0a..f4de13b6 100644 --- a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart +++ b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart @@ -1,4 +1,6 @@ import 'dart:async'; +import 'dart:io'; +import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -371,13 +373,31 @@ class _StreamAttachmentPickerState extends State { void _addAssetAttachment(AssetEntity medium) async { final mediaFile = await medium.originFile; - if (mediaFile == null) return; - final tempDir = await getTemporaryDirectory(); - final cachedFile = await mediaFile - .copy('${tempDir.path}/${mediaFile.path.split('/').last}'); + // TODO: Confirm that this max resolution is final + const maxCDNImageResolution = 16800000; + final imageResolution = medium.width * medium.height; + File? cachedFile; + if (imageResolution > maxCDNImageResolution) { + final aspect = imageResolution / maxCDNImageResolution; + final updatedSize = medium.size / (math.sqrt(aspect)); + final resizedImage = await medium.thumbnailDataWithSize( + ThumbnailSize( + updatedSize.width.floor(), + updatedSize.height.floor(), + ), + quality: 30, // TODO: investigate compressing all images + ); + cachedFile = + await File('${tempDir.path}/${mediaFile.path.split('/').last}') + .create() + ..writeAsBytesSync(resizedImage!); + } else { + cachedFile = await mediaFile + .copy('${tempDir.path}/${mediaFile.path.split('/').last}'); + } final file = AttachmentFile( path: cachedFile.path, From 10fffc335f0c0e81839a9000f52ba20c836d782d Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 16 Aug 2022 15:27:11 +0530 Subject: [PATCH 30/49] feat(core): Add `ChannelListController.filter` and `ChannelListController.sort` setters. Signed-off-by: xsahil03x --- .../stream_chat_flutter_core/CHANGELOG.md | 7 ++++ .../src/stream_channel_list_controller.dart | 39 ++++++++++++++++--- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index 3d6aa989..edd187dc 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,8 +1,15 @@ ## Upcoming +🐞 Fixed + - [#1269](https://github.com/GetStream/stream-chat-flutter/issues/1269) Fix `ChannelListEventHandler` castError at PagedValue.asSuccess. +✅ Added + +- Added setters to update `ChannelListController.filter` and `ChannelListController.sort` properties + in order to support runtime changes. + ## 4.4.1 - Updated `stream_chat` dependency to [`4.4.1`](https://pub.dev/packages/stream_chat/changelog). diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart index bea403be..484b1f22 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart @@ -50,7 +50,9 @@ class StreamChannelListController extends PagedValueNotifier { this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, - }) : _eventHandler = eventHandler ?? StreamChannelListEventHandler(), + }) : _activeFilter = filter, + _activeSort = sort, + _eventHandler = eventHandler ?? StreamChannelListEventHandler(), super(const PagedValue.loading()); /// Creates a [StreamChannelListController] from the passed [value]. @@ -64,7 +66,9 @@ class StreamChannelListController extends PagedValueNotifier { this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, - }) : _eventHandler = eventHandler ?? StreamChannelListEventHandler(); + }) : _activeFilter = filter, + _activeSort = sort, + _eventHandler = eventHandler ?? StreamChannelListEventHandler(); /// The client to use for the channels list. final StreamChatClient client; @@ -78,6 +82,7 @@ class StreamChannelListController extends PagedValueNotifier { /// /// You can also filter other built-in channel fields. final Filter? filter; + Filter? _activeFilter; /// The sorting used for the channels matching the filters. /// @@ -89,6 +94,7 @@ class StreamChannelListController extends PagedValueNotifier { /// /// Direction can be ascending or descending. final List>? sort; + List>? _activeSort; /// If true you’ll receive user presence updates via the websocket events final bool presence; @@ -103,6 +109,18 @@ class StreamChannelListController extends PagedValueNotifier { /// Number of members to fetch in each channel. final int? memberLimit; + /// Allows for the change of filters used for channel queries. + /// + /// Use this if you need to support runtime filter changes, + /// through custom filters UI. + set filter(Filter? value) => _activeFilter = value; + + /// Allows for the change of sort used for channel queries. + /// + /// Use this if you need to support runtime sort changes, + /// through custom sort UI. + set sort(List>? value) => _activeSort = value; + @override Future doInitialLoad() async { final limit = min( @@ -111,8 +129,8 @@ class StreamChannelListController extends PagedValueNotifier { ); try { await for (final channels in client.queryChannels( - filter: filter, - sort: sort, + filter: _activeFilter, + sort: _activeSort, memberLimit: memberLimit, messageLimit: messageLimit, presence: presence, @@ -140,8 +158,8 @@ class StreamChannelListController extends PagedValueNotifier { try { await for (final channels in client.queryChannels( - filter: filter, - sort: sort, + filter: _activeFilter, + sort: _activeSort, memberLimit: memberLimit, messageLimit: messageLimit, presence: presence, @@ -163,6 +181,15 @@ class StreamChannelListController extends PagedValueNotifier { } } + @override + Future refresh({bool resetValue = true}) { + if (resetValue) { + _activeFilter = filter; + _activeSort = sort; + } + return super.refresh(resetValue: resetValue); + } + /// Replaces the previously loaded channels with [channels] and updates /// the nextPageKey. set channels(List channels) { From 0117e4cb7c18fdf1620a0143cb8c676d46ac74fc Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 16 Aug 2022 15:32:10 +0530 Subject: [PATCH 31/49] fix(core): use copyWith while setting controller items. Signed-off-by: xsahil03x --- packages/stream_chat_flutter_core/CHANGELOG.md | 2 ++ .../lib/src/stream_channel_list_controller.dart | 13 +++++++------ .../lib/src/stream_user_list_controller.dart | 13 +++++++------ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index 3d6aa989..5335a023 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -2,6 +2,8 @@ - [#1269](https://github.com/GetStream/stream-chat-flutter/issues/1269) Fix `ChannelListEventHandler` castError at PagedValue.asSuccess. +- [#1241](https://github.com/GetStream/stream-chat-flutter/issues/1241) StreamChannelListView load + more indicator non stop. ## 4.4.1 diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart index bea403be..7cd425a6 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart @@ -163,13 +163,14 @@ class StreamChannelListController extends PagedValueNotifier { } } - /// Replaces the previously loaded channels with [channels] and updates - /// the nextPageKey. + /// Replaces the previously loaded channels with the passed [channels]. set channels(List channels) { - value = PagedValue( - items: channels, - nextPageKey: channels.length, - ); + if (value.isSuccess) { + final currentValue = value.asSuccess; + value = currentValue.copyWith(items: channels); + } else { + value = PagedValue(items: channels); + } } /// Returns/Creates a new Channel and starts watching it. diff --git a/packages/stream_chat_flutter_core/lib/src/stream_user_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_user_list_controller.dart index 3e1b2892..466b6177 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_user_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_user_list_controller.dart @@ -153,12 +153,13 @@ class StreamUserListController extends PagedValueNotifier { return super.refresh(resetValue: resetValue); } - /// Replaces the previously loaded users with [users] and updates - /// the nextPageKey. + /// Replaces the previously loaded users with the passed [users]. set users(List users) { - value = PagedValue( - items: users, - nextPageKey: users.length, - ); + if (value.isSuccess) { + final currentValue = value.asSuccess; + value = currentValue.copyWith(items: users); + } else { + value = PagedValue(items: users); + } } } From ae199c636abda48c5e19572750a1e0b5dec501b6 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 19 Aug 2022 12:04:50 +0530 Subject: [PATCH 32/49] feat(ui): Added `thumbnailSize`, `thumbnailResizeType`, and `thumbnailCropType` params to `StreamMessageWidget` and `StreamAttachmentPicker` to customize the appearance of image thumbnails. Signed-off-by: xsahil03x --- packages/stream_chat_flutter/CHANGELOG.md | 39 +++ .../lib/src/attachment/image_attachment.dart | 58 ++-- .../lib/src/extension.dart | 47 ++++ .../lib/src/full_screen_media.dart | 265 +++++++++--------- .../lib/src/image_group.dart | 21 +- .../lib/src/media_list_view.dart | 114 ++++++-- .../lib/src/message_widget.dart | 33 +++ .../stream_attachment_picker.dart | 65 ++++- .../message_input/stream_message_input.dart | 1 + .../lib/stream_chat_flutter.dart | 4 + 10 files changed, 453 insertions(+), 194 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index be7ffa2b..4482e3ac 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -4,6 +4,45 @@ - [[#882]](https://github.com/GetStream/stream-chat-flutter/issues/882) Lots of unhandled exceptions when network is off or spotty. +- Fixes an error where Stream CDN images were not being resized in the message list view. + +🚀 Improved + +- Automatically resize images that are above a specific pixel count to ensure resizing works: + getstream.io/chat/docs/go-golang/file_uploads/#image-resizing + +✅ Added + +- Added `thumbnailSize`, `thumbnailResizeType`, and `thumbnailCropType` params + to `StreamMessageWidget` and `StreamAttachmentPicker` to customize the appearance of image + thumbnails. + + ```dart + StreamMessageInput( + focusNode: _focusNode, + messageInputController: _messageInputController, + attachmentsPickerBuilder: (_, __, picker) { + return picker.copyWith( + attachmentThumbnailSize: ..., + attachmentThumbnailFormat: ..., + attachmentThumbnailQuality: ..., + attachmentThumbnailScale: ..., + ); + }, + ), + ``` + + ```dart + StreamMessageListView( + messageBuilder: (context, details, messages, defaultMessage) { + return defaultMessage.copyWith( + imageAttachmentThumbnailSize: ..., + imageAttachmentThumbnailCropType: ..., + imageAttachmentThumbnailResizeType: ..., + ); + }, + ), + ``` ## 4.4.1 diff --git a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart index a5537140..8d0d3f37 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_title.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// {@macro image_attachment} @@ -24,6 +25,9 @@ class StreamImageAttachment extends StreamAttachmentWidget { this.onShowMessage, this.onReturnAction, this.onAttachmentTap, + this.imageThumbnailSize = const Size(400, 400), + this.imageThumbnailResizeType = 'crop', + this.imageThumbnailCropType = 'center', }); /// [StreamMessageThemeData] for showing image title @@ -41,6 +45,19 @@ class StreamImageAttachment extends StreamAttachmentWidget { /// Callback when attachment is tapped final VoidCallback? onAttachmentTap; + /// Size of the attachment image thumbnail. + final Size imageThumbnailSize; + + /// Resize type of the image attachment thumbnail. + /// + /// Defaults to [crop] + final String /*clip|crop|scale|fill*/ imageThumbnailResizeType; + + /// Crop type of the image attachment thumbnail. + /// + /// Defaults to [center] + final String /*center|top|bottom|left|right*/ imageThumbnailCropType; + @override Widget build(BuildContext context) => source.when( local: () { @@ -65,43 +82,22 @@ class StreamImageAttachment extends StreamAttachmentWidget { var imageUrl = attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl; - if (imageUrl == null) { - return AttachmentError(size: size); - } + if (imageUrl == null) return AttachmentError(size: size); - var imageUri = Uri.parse(imageUrl); - if (imageUri.host.endsWith('stream-io-cdn.com') && - (imageUri.queryParameters['h'] == null || - imageUri.queryParameters['h'] == '*') && - (imageUri.queryParameters['w'] == null || - imageUri.queryParameters['w'] == '*') && - (imageUri.queryParameters['crop'] == null || - imageUri.queryParameters['crop'] == '*') && - (imageUri.queryParameters['resize'] == null || - imageUri.queryParameters['resize'] == '*')) { - imageUri = imageUri.replace(queryParameters: { - ...imageUri.queryParameters, - 'h': '400', // TODO: Are these sizes optimal? Consider web/desktop - 'w': '400', - 'crop': 'center', - 'resize': 'clip', - }); - } else if (imageUri.host.endsWith('stream-cloud-uploads.imgix.net')) { - imageUri = imageUri.replace(queryParameters: { - ...imageUri.queryParameters, - 'height': '400', - 'width': '400', - 'fit': 'crop', - }); - } - imageUrl = imageUri.toString(); + imageUrl = imageUrl.getResizedImageUrl( + width: imageThumbnailSize.width, + height: imageThumbnailSize.height, + resize: imageThumbnailResizeType, + crop: imageThumbnailCropType, + ); return _buildImageAttachment( context, CachedNetworkImage( - cacheKey: imageUri.replace(queryParameters: {}).toString(), + imageUrl: imageUrl, height: size?.height, width: size?.width, + fit: BoxFit.cover, placeholder: (context, __) { final image = Image.asset( 'images/placeholder.png', @@ -115,9 +111,7 @@ class StreamImageAttachment extends StreamAttachmentWidget { child: image, ); }, - imageUrl: imageUrl, errorWidget: (context, url, error) => AttachmentError(size: size), - fit: BoxFit.cover, ), ); }, diff --git a/packages/stream_chat_flutter/lib/src/extension.dart b/packages/stream_chat_flutter/lib/src/extension.dart index 5ad30039..c9566b0d 100644 --- a/packages/stream_chat_flutter/lib/src/extension.dart +++ b/packages/stream_chat_flutter/lib/src/extension.dart @@ -31,6 +31,46 @@ extension StringExtension on String { /// Levenshtein distance between this and [t]. int levenshteinDistance(String t) => levenshtein(this, t); + + /// Returns a resized imageUrl with the given [width], [height], [resize] + /// and [crop] if it is from Stream CDN or Dashboard. + /// + /// Read more at https://getstream.io/chat/docs/flutter-dart/file_uploads/?language=dart#image-resizing + String getResizedImageUrl({ + // TODO: Are these sizes optimal? Consider web/desktop + double width = 400, + double height = 400, + String /*clip|crop|scale|fill*/ resize = 'scale', + String /*center|top|bottom|left|right*/ crop = 'center', + }) { + final uri = Uri.parse(this); + final host = uri.host; + + final fromStreamCDN = host.endsWith('stream-io-cdn.com'); + final fromStreamDashboard = host.endsWith('stream-cloud-uploads.imgix.net'); + + if (!fromStreamCDN && !fromStreamDashboard) return this; + + final queryParameters = {...uri.queryParameters}; + + if (fromStreamCDN) { + if (queryParameters['h'].isNullOrMatches('*') && + queryParameters['w'].isNullOrMatches('*') && + queryParameters['crop'].isNullOrMatches('*') && + queryParameters['resize'].isNullOrMatches('*')) { + queryParameters['h'] = height.floor().toString(); + queryParameters['w'] = width.floor().toString(); + queryParameters['crop'] = crop; + queryParameters['resize'] = resize; + } + } else if (fromStreamDashboard) { + queryParameters['height'] = height.floor().toString(); + queryParameters['width'] = width.floor().toString(); + queryParameters['fit'] = crop; + } + + return uri.replace(queryParameters: queryParameters).toString(); + } } /// List extension @@ -281,3 +321,10 @@ extension UriX on Uri { return Uri.parse('http://${toString()}'); } } + +/// Extensions on generic type [T] +extension TypeX on T? { + /// Returns true if the value is null or matches the given [value] + /// otherwise returns false. + bool isNullOrMatches(T value) => this == null || this == value; +} diff --git a/packages/stream_chat_flutter/lib/src/full_screen_media.dart b/packages/stream_chat_flutter/lib/src/full_screen_media.dart index cb54c464..55120de0 100644 --- a/packages/stream_chat_flutter/lib/src/full_screen_media.dart +++ b/packages/stream_chat_flutter/lib/src/full_screen_media.dart @@ -5,6 +5,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:chewie/chewie.dart'; import 'package:flutter/material.dart'; import 'package:photo_view/photo_view.dart'; +import 'package:shimmer/shimmer.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:video_player/video_player.dart'; @@ -64,36 +65,21 @@ class StreamFullScreenMedia extends StatefulWidget { _StreamFullScreenMediaState createState() => _StreamFullScreenMediaState(); } -class _StreamFullScreenMediaState extends State - with SingleTickerProviderStateMixin { - late final AnimationController _animationController; +class _StreamFullScreenMediaState extends State { late final PageController _pageController; - late final _curvedAnimation = CurvedAnimation( - parent: _animationController, - curve: Curves.easeOut, - reverseCurve: Curves.easeIn, - ); + late final _currentPage = ValueNotifier(widget.startIndex); + late final _isDisplayingDetail = ValueNotifier(true); - final _opacityTween = Tween(begin: 1, end: 0); - late final _opacityAnimation = _opacityTween.animate( - CurvedAnimation( - parent: _animationController, - curve: const Interval(0, 0.6, curve: Curves.easeOut), - ), - ); - - late final ValueNotifier _currentPage = ValueNotifier(widget.startIndex); + void switchDisplayingDetail() { + _isDisplayingDetail.value = !_isDisplayingDetail.value; + } final videoPackages = {}; @override void initState() { super.initState(); - _animationController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 300), - ); _pageController = PageController(initialPage: widget.startIndex); for (var i = 0; i < widget.mediaAttachmentPackages.length; i++) { final attachment = widget.mediaAttachmentPackages[i].attachment; @@ -127,26 +113,102 @@ class _StreamFullScreenMediaState extends State @override Widget build(BuildContext context) => Scaffold( resizeToAvoidBottomInset: false, - body: Stack( - children: [ - PageView.builder( + body: ValueListenableBuilder( + valueListenable: _currentPage, + builder: (context, currentPage, child) { + final _currentAttachmentPackage = + widget.mediaAttachmentPackages[currentPage]; + final _currentMessage = _currentAttachmentPackage.message; + final _currentAttachment = _currentAttachmentPackage.attachment; + return Stack( + children: [ + Positioned.fill(child: child!), + ValueListenableBuilder( + valueListenable: _isDisplayingDetail, + builder: (context, isDisplayingDetail, child) { + final mediaQuery = MediaQuery.of(context); + final topPadding = mediaQuery.padding.top; + return AnimatedPositionedDirectional( + duration: kThemeAnimationDuration, + curve: Curves.easeInOut, + top: isDisplayingDetail + ? 0 + : -(topPadding + kToolbarHeight), + start: 0, + end: 0, + height: topPadding + kToolbarHeight, + child: StreamGalleryHeader( + userName: widget.userName, + sentAt: context.translations.sentAtText( + date: _currentAttachmentPackage.message.createdAt, + time: _currentAttachmentPackage.message.createdAt, + ), + onBackPressed: Navigator.of(context).pop, + message: _currentMessage, + attachment: _currentAttachment, + onShowMessage: () { + widget.onShowMessage?.call( + _currentMessage, + StreamChannel.of(context).channel, + ); + }, + attachmentActionsModalBuilder: + widget.attachmentActionsModalBuilder, + ), + ); + }, + ), + if (!_currentMessage.isEphemeral) + ValueListenableBuilder( + valueListenable: _isDisplayingDetail, + builder: (context, isDisplayingDetail, child) { + final mediaQuery = MediaQuery.of(context); + final bottomPadding = mediaQuery.padding.bottom; + return AnimatedPositionedDirectional( + duration: kThemeAnimationDuration, + curve: Curves.easeInOut, + bottom: isDisplayingDetail + ? 0 + : -(bottomPadding + kToolbarHeight), + start: 0, + end: 0, + height: bottomPadding + kToolbarHeight, + child: StreamGalleryFooter( + currentPage: currentPage, + totalPages: widget.mediaAttachmentPackages.length, + mediaAttachmentPackages: + widget.mediaAttachmentPackages, + mediaSelectedCallBack: (val) { + _currentPage.value = val; + _pageController.animateToPage( + val, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + Navigator.pop(context); + }, + ), + ); + }, + ), + ], + ); + }, + child: InkWell( + onTap: switchDisplayingDetail, + child: PageView.builder( controller: _pageController, + itemCount: widget.mediaAttachmentPackages.length, onPageChanged: (val) { _currentPage.value = val; - - if (videoPackages.isEmpty) { - return; - } - + if (videoPackages.isEmpty) return; final currentAttachment = widget.mediaAttachmentPackages[val].attachment; - for (final e in videoPackages.values) { if (e._attachment != currentAttachment) { e._chewieController?.pause(); } } - if (widget.autoplayVideos && currentAttachment.type == 'video') { final controller = videoPackages[currentAttachment.id]!; @@ -161,33 +223,44 @@ class _StreamFullScreenMediaState extends State final imageUrl = attachment.imageUrl ?? attachment.assetUrl ?? attachment.thumbUrl; - return AnimatedBuilder( - animation: _curvedAnimation, - builder: (context, child) => PhotoView( - loadingBuilder: (context, image) => const Offstage(), - imageProvider: (imageUrl == null && - attachment.localUri != null && - attachment.file?.bytes != null) - ? Image.memory(attachment.file!.bytes!).image - : CachedNetworkImageProvider(imageUrl!), - maxScale: PhotoViewComputedScale.covered, - minScale: PhotoViewComputedScale.contained, - heroAttributes: PhotoViewHeroAttributes( - tag: widget.mediaAttachmentPackages, + return ValueListenableBuilder( + valueListenable: _isDisplayingDetail, + builder: (context, isDisplayingDetail, _) => + AnimatedContainer( + color: isDisplayingDetail + ? StreamChannelHeaderTheme.of(context).color + : Colors.black, + duration: kThemeAnimationDuration, + child: PhotoView( + imageProvider: (imageUrl == null && + attachment.localUri != null && + attachment.file?.bytes != null) + ? Image.memory(attachment.file!.bytes!).image + : CachedNetworkImageProvider(imageUrl!), + errorBuilder: (_, __, ___) => const AttachmentError(), + loadingBuilder: (context, _) { + final image = Image.asset( + 'images/placeholder.png', + fit: BoxFit.cover, + package: 'stream_chat_flutter', + ); + final colorTheme = + StreamChatTheme.of(context).colorTheme; + return Shimmer.fromColors( + baseColor: colorTheme.disabled, + highlightColor: colorTheme.inputBg, + child: image, + ); + }, + maxScale: PhotoViewComputedScale.covered, + minScale: PhotoViewComputedScale.contained, + heroAttributes: PhotoViewHeroAttributes( + tag: widget.mediaAttachmentPackages, + ), + backgroundDecoration: const BoxDecoration( + color: Colors.transparent, + ), ), - backgroundDecoration: BoxDecoration( - color: ColorTween( - begin: StreamChannelHeaderTheme.of(context).color, - end: Colors.black, - ).lerp(_curvedAnimation.value), - ), - onTapUp: (a, b, c) { - if (_animationController.isCompleted) { - _animationController.reverse(); - } else { - _animationController.forward(); - } - }, ), ); } else if (attachment.type == 'video') { @@ -198,17 +271,9 @@ class _StreamFullScreenMediaState extends State ); } return InkWell( - onTap: () { - if (_animationController.isCompleted) { - _animationController.reverse(); - } else { - _animationController.forward(); - } - }, + onTap: switchDisplayingDetail, child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 50, - ), + padding: const EdgeInsets.symmetric(vertical: 50), child: Chewie( controller: controller.chewieController!, ), @@ -217,76 +282,16 @@ class _StreamFullScreenMediaState extends State } return const SizedBox(); }, - itemCount: widget.mediaAttachmentPackages.length, ), - FadeTransition( - opacity: _opacityAnimation, - child: ValueListenableBuilder( - valueListenable: _currentPage, - builder: (context, value, child) { - final _currentAttachmentPackage = - widget.mediaAttachmentPackages[value]; - final _currentMessage = _currentAttachmentPackage.message; - final _currentAttachment = - _currentAttachmentPackage.attachment; - return Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - StreamGalleryHeader( - userName: widget.userName, - sentAt: context.translations.sentAtText( - date: widget - .mediaAttachmentPackages[_currentPage.value] - .message - .createdAt, - time: widget - .mediaAttachmentPackages[_currentPage.value] - .message - .createdAt, - ), - onBackPressed: () { - Navigator.of(context).pop(); - }, - message: _currentMessage, - attachment: _currentAttachment, - onShowMessage: () { - widget.onShowMessage?.call( - _currentMessage, - StreamChannel.of(context).channel, - ); - }, - attachmentActionsModalBuilder: - widget.attachmentActionsModalBuilder, - ), - if (!_currentMessage.isEphemeral) - StreamGalleryFooter( - currentPage: value, - totalPages: widget.mediaAttachmentPackages.length, - mediaAttachmentPackages: - widget.mediaAttachmentPackages, - mediaSelectedCallBack: (val) { - _currentPage.value = val; - _pageController.animateToPage( - val, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - Navigator.pop(context); - }, - ), - ], - ); - }, - ), - ), - ], + ), ), ); @override void dispose() { - _animationController.dispose(); + _currentPage.dispose(); _pageController.dispose(); + _isDisplayingDetail.dispose(); for (final package in videoPackages.values) { package.dispose(); } diff --git a/packages/stream_chat_flutter/lib/src/image_group.dart b/packages/stream_chat_flutter/lib/src/image_group.dart index cb8f6edd..16cb7352 100644 --- a/packages/stream_chat_flutter/lib/src/image_group.dart +++ b/packages/stream_chat_flutter/lib/src/image_group.dart @@ -19,6 +19,9 @@ class StreamImageGroup extends StatelessWidget { this.onReturnAction, this.onShowMessage, this.onAttachmentTap, + this.imageThumbnailSize = const Size(400, 400), + this.imageThumbnailResizeType = 'crop', + this.imageThumbnailCropType = 'center', }); /// List of attachments to show @@ -36,12 +39,25 @@ class StreamImageGroup extends StatelessWidget { /// [StreamMessageThemeData] to apply to message final StreamMessageThemeData messageTheme; - /// Size of iamges + /// Size of images final Size size; /// Callback for when show message is tapped final ShowMessageCallback? onShowMessage; + /// Size of the attachment image thumbnail. + final Size imageThumbnailSize; + + /// Resize type of the image attachment thumbnail. + /// + /// Defaults to [crop] + final String /*clip|crop|scale|fill*/ imageThumbnailResizeType; + + /// Crop type of the image attachment thumbnail. + /// + /// Defaults to [center] + final String /*center|top|bottom|left|right*/ imageThumbnailCropType; + @override Widget build(BuildContext context) => ConstrainedBox( constraints: BoxConstraints.loose(size), @@ -153,5 +169,8 @@ class StreamImageGroup extends StatelessWidget { message: message, messageTheme: messageTheme, onAttachmentTap: () => _onTap(context, index), + imageThumbnailSize: imageThumbnailSize, + imageThumbnailResizeType: imageThumbnailResizeType, + imageThumbnailCropType: imageThumbnailCropType, ); } diff --git a/packages/stream_chat_flutter/lib/src/media_list_view.dart b/packages/stream_chat_flutter/lib/src/media_list_view.dart index 34b512c4..7dce7864 100644 --- a/packages/stream_chat_flutter/lib/src/media_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/media_list_view.dart @@ -22,6 +22,10 @@ class StreamMediaListView extends StatefulWidget { this.selectedIds = const [], this.onSelect, this.controller, + this.thumbnailSize = const ThumbnailSize(400, 400), + this.thumbnailFormat = ThumbnailFormat.jpeg, + this.thumbnailQuality = 100, + this.thumbnailScale = 1, }); /// Stores the media selected @@ -33,6 +37,21 @@ class StreamMediaListView extends StatefulWidget { /// Controller that handles MediaListView final MediaListViewController? controller; + /// The thumbnail size. + final ThumbnailSize thumbnailSize; + + /// {@macro photo_manager.ThumbnailFormat} + final ThumbnailFormat thumbnailFormat; + + /// The quality value for the thumbnail. + /// + /// Valid from 1 to 100. + /// Defaults to 100. + final int thumbnailQuality; + + /// Scale of the image. + final double thumbnailScale; + @override _StreamMediaListViewState createState() => _StreamMediaListViewState(); } @@ -75,14 +94,18 @@ class _StreamMediaListViewState extends State { AspectRatio( aspectRatio: 1, child: FadeInImage( + image: MediaThumbnailProvider( + media: media, + size: widget.thumbnailSize, + format: widget.thumbnailFormat, + quality: widget.thumbnailQuality, + scale: widget.thumbnailScale, + ), fadeInDuration: const Duration(milliseconds: 300), placeholder: const AssetImage( 'images/placeholder.png', package: 'stream_chat_flutter', ), - image: MediaThumbnailProvider( - media: media, - ), fit: BoxFit.cover, ), ), @@ -195,55 +218,95 @@ class _StreamMediaListViewState extends State { } } -/// ImageProvider implementation +/// ImageProvider implementation for [AssetEntity]. class MediaThumbnailProvider extends ImageProvider { /// Constructor for creating a [MediaThumbnailProvider] const MediaThumbnailProvider({ required this.media, + // TODO: Are these sizes optimal? Consider web/desktop + this.size = const ThumbnailSize(400, 400), + this.format = ThumbnailFormat.jpeg, + this.quality = 100, + this.scale = 2, }); - /// Media to load + /// Media to get the thumbnail from. final AssetEntity media; + /// The thumbnail size. + final ThumbnailSize size; + + /// {@macro photo_manager.ThumbnailFormat} + final ThumbnailFormat format; + + /// The quality value for the thumbnail. + /// + /// Valid from 1 to 100. + /// Defaults to 100. + final int quality; + + /// Scale of the image. + final double scale; + + @override + Future obtainKey(ImageConfiguration configuration) { + return SynchronousFuture(this); + } + @override ImageStreamCompleter load( MediaThumbnailProvider key, DecoderCallback decode, - ) => - MultiFrameImageStreamCompleter( - codec: _loadAsync(key, decode), - scale: 1, - informationCollector: () sync* { - yield ErrorDescription('Id: ${media.id}'); - }, - ); + ) { + return MultiFrameImageStreamCompleter( + codec: _loadAsync(key, decode), + scale: key.scale, + informationCollector: () sync* { + yield DiagnosticsProperty( + 'Thumbnail provider: $this \n Thumbnail key: $key', + this, + style: DiagnosticsTreeStyle.errorProperty, + ); + }, + ); + } Future _loadAsync( MediaThumbnailProvider key, DecoderCallback decode, ) async { - assert(key == this, 'Checks MediaThumbnailProvider'); - final bytes = await media.thumbnailData; - + assert(key == this, '$key is not $this'); + final bytes = await media.thumbnailDataWithSize( + size, + format: format, + quality: quality, + ); return decode(bytes!); } - @override - Future obtainKey(ImageConfiguration configuration) => - SynchronousFuture(this); - @override bool operator ==(dynamic other) { - if (other.runtimeType != runtimeType) return false; - final MediaThumbnailProvider typedOther = other; - return media.id == typedOther.media.id; + if (other is MediaThumbnailProvider) { + return media == other.media && + size == other.size && + format == other.format && + quality == other.quality && + scale == other.scale; + } + return false; } @override - int get hashCode => media.id.hashCode; + int get hashCode => hashValues(media, size, format, quality, scale); @override - String toString() => '$runtimeType("${media.id}")'; + String toString() => '$runtimeType(' + 'media: $media, ' + 'size: $size, ' + 'format: $format, ' + 'quality: $quality, ' + 'scale: $scale' + ')'; } extension on Duration { @@ -252,7 +315,6 @@ extension on Duration { if (s.startsWith('00:')) { return s.replaceFirst('00:', ''); } - return s; } } diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index b247c91e..4f926cd1 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -105,6 +105,9 @@ class StreamMessageWidget extends StatefulWidget { this.customActions = const [], this.onAttachmentTap, this.usernameBuilder, + this.imageAttachmentThumbnailSize = const Size(400, 400), + this.imageAttachmentThumbnailResizeType = 'crop', + this.imageAttachmentThumbnailCropType = 'center', }) : attachmentBuilders = { 'image': (context, message, attachments) { final border = RoundedRectangleBorder( @@ -130,6 +133,10 @@ class StreamMessageWidget extends StatefulWidget { onShowMessage: onShowMessage, onReturnAction: onReturnAction, onAttachmentTap: onAttachmentTap, + imageThumbnailSize: imageAttachmentThumbnailSize, + imageThumbnailResizeType: + imageAttachmentThumbnailResizeType, + imageThumbnailCropType: imageAttachmentThumbnailCropType, ), ), border, @@ -155,6 +162,9 @@ class StreamMessageWidget extends StatefulWidget { onAttachmentTap.call(message, attachments[0]); } : null, + imageThumbnailSize: imageAttachmentThumbnailSize, + imageThumbnailResizeType: imageAttachmentThumbnailResizeType, + imageThumbnailCropType: imageAttachmentThumbnailCropType, ), border, reverse, @@ -413,6 +423,19 @@ class StreamMessageWidget extends StatefulWidget { /// Customize onTap on attachment final void Function(Message message, Attachment attachment)? onAttachmentTap; + /// Size of the image attachment thumbnail. + final Size imageAttachmentThumbnailSize; + + /// Resize type of the image attachment thumbnail. + /// + /// Defaults to [crop] + final String /*clip|crop|scale|fill*/ imageAttachmentThumbnailResizeType; + + /// Crop type of the image attachment thumbnail. + /// + /// Defaults to [center] + final String /*center|top|bottom|left|right*/ imageAttachmentThumbnailCropType; + /// Creates a copy of [StreamMessageWidget] with /// specified attributes overridden. StreamMessageWidget copyWith({ @@ -468,6 +491,9 @@ class StreamMessageWidget extends StatefulWidget { List? customActions, void Function(Message message, Attachment attachment)? onAttachmentTap, Widget Function(BuildContext, User)? userAvatarBuilder, + Size? imageAttachmentThumbnailSize, + String? imageAttachmentThumbnailResizeType, + String? imageAttachmentThumbnailCropType, }) => StreamMessageWidget( key: key ?? this.key, @@ -528,6 +554,13 @@ class StreamMessageWidget extends StatefulWidget { customActions: customActions ?? this.customActions, onAttachmentTap: onAttachmentTap ?? this.onAttachmentTap, userAvatarBuilder: userAvatarBuilder ?? this.userAvatarBuilder, + imageAttachmentThumbnailSize: + imageAttachmentThumbnailSize ?? this.imageAttachmentThumbnailSize, + imageAttachmentThumbnailResizeType: + imageAttachmentThumbnailResizeType ?? + this.imageAttachmentThumbnailResizeType, + imageAttachmentThumbnailCropType: imageAttachmentThumbnailCropType ?? + this.imageAttachmentThumbnailCropType, ); @override diff --git a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart index f4de13b6..8c1e14f8 100644 --- a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart +++ b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart @@ -43,6 +43,10 @@ class StreamAttachmentPicker extends StatefulWidget { DefaultAttachmentTypes.video, ], this.customAttachmentTypes = const [], + this.attachmentThumbnailSize = const ThumbnailSize(400, 400), + this.attachmentThumbnailFormat = ThumbnailFormat.jpeg, + this.attachmentThumbnailQuality = 100, + this.attachmentThumbnailScale = 1, }); /// True if the picker is open. @@ -77,6 +81,25 @@ class StreamAttachmentPicker extends StatefulWidget { /// The list of custom attachment types that can be picked. final List customAttachmentTypes; + /// Size of the attachment thumbnails. + /// + /// Defaults to (400, 400). + final ThumbnailSize attachmentThumbnailSize; + + /// Format of the attachment thumbnails. + /// + /// Defaults to [ThumbnailFormat.jpeg]. + final ThumbnailFormat attachmentThumbnailFormat; + + /// The quality value for the attachment thumbnails. + /// + /// Valid from 1 to 100. + /// Defaults to 100. + final int attachmentThumbnailQuality; + + /// The scale to apply on the [attachmentThumbnailSize]. + final double attachmentThumbnailScale; + /// Used to create a new copy of [StreamAttachmentPicker] with modified /// properties. StreamAttachmentPicker copyWith({ @@ -91,7 +114,11 @@ class StreamAttachmentPicker extends StatefulWidget { ValueChanged? onChangeInputState, ValueChanged? onError, List? allowedAttachmentTypes, - List? customAttachmentTypes = const [], + List? customAttachmentTypes, + ThumbnailSize? attachmentThumbnailSize, + ThumbnailFormat? attachmentThumbnailFormat, + int? attachmentThumbnailQuality, + double? attachmentThumbnailScale, }) => StreamAttachmentPicker( key: key ?? this.key, @@ -109,6 +136,14 @@ class StreamAttachmentPicker extends StatefulWidget { allowedAttachmentTypes ?? this.allowedAttachmentTypes, customAttachmentTypes: customAttachmentTypes ?? this.customAttachmentTypes, + attachmentThumbnailSize: + attachmentThumbnailSize ?? this.attachmentThumbnailSize, + attachmentThumbnailFormat: + attachmentThumbnailFormat ?? this.attachmentThumbnailFormat, + attachmentThumbnailQuality: + attachmentThumbnailQuality ?? this.attachmentThumbnailQuality, + attachmentThumbnailScale: + attachmentThumbnailScale ?? this.attachmentThumbnailScale, ); @override @@ -360,6 +395,11 @@ class _StreamAttachmentPickerState extends State { }, allowedAttachmentTypes: widget.allowedAttachmentTypes, customAttachmentTypes: widget.customAttachmentTypes, + mediaThumbnailSize: widget.attachmentThumbnailSize, + mediaThumbnailFormat: widget.attachmentThumbnailFormat, + mediaThumbnailQuality: + widget.attachmentThumbnailQuality, + mediaThumbnailScale: widget.attachmentThumbnailScale, ), ), ), @@ -374,9 +414,11 @@ class _StreamAttachmentPickerState extends State { void _addAssetAttachment(AssetEntity medium) async { final mediaFile = await medium.originFile; if (mediaFile == null) return; + final tempDir = await getTemporaryDirectory(); // TODO: Confirm that this max resolution is final + // Taken from https://getstream.io/chat/docs/flutter-dart/file_uploads/?language=dart#image-resizing const maxCDNImageResolution = 16800000; final imageResolution = medium.width * medium.height; File? cachedFile; @@ -388,12 +430,13 @@ class _StreamAttachmentPickerState extends State { updatedSize.width.floor(), updatedSize.height.floor(), ), - quality: 30, // TODO: investigate compressing all images + quality: 70, // TODO: investigate compressing all images ); - cachedFile = + final file = await File('${tempDir.path}/${mediaFile.path.split('/').last}') - .create() - ..writeAsBytesSync(resizedImage!); + .create(); + file.writeAsBytesSync(resizedImage!); + cachedFile = file; } else { cachedFile = await mediaFile .copy('${tempDir.path}/${mediaFile.path.split('/').last}'); @@ -457,6 +500,10 @@ class _PickerWidget extends StatefulWidget { required this.allowedAttachmentTypes, required this.customAttachmentTypes, required this.mediaListViewController, + this.mediaThumbnailSize = const ThumbnailSize(400, 400), + this.mediaThumbnailFormat = ThumbnailFormat.jpeg, + this.mediaThumbnailQuality = 100, + this.mediaThumbnailScale = 1, }); final int filePickerIndex; @@ -468,6 +515,10 @@ class _PickerWidget extends StatefulWidget { final List allowedAttachmentTypes; final List customAttachmentTypes; final MediaListViewController mediaListViewController; + final ThumbnailSize mediaThumbnailSize; + final ThumbnailFormat mediaThumbnailFormat; + final int mediaThumbnailQuality; + final double mediaThumbnailScale; @override _PickerWidgetState createState() => _PickerWidgetState(); @@ -522,6 +573,10 @@ class _PickerWidgetState extends State<_PickerWidget> { selectedIds: widget.selectedMedias, onSelect: widget.onMediaSelected, controller: widget.mediaListViewController, + thumbnailSize: widget.mediaThumbnailSize, + thumbnailFormat: widget.mediaThumbnailFormat, + thumbnailQuality: widget.mediaThumbnailQuality, + thumbnailScale: widget.mediaThumbnailScale, ); } diff --git a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart index db1c1925..9b7fe009 100644 --- a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart +++ b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart @@ -10,6 +10,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:photo_manager/photo_manager.dart'; import 'package:shimmer/shimmer.dart'; import 'package:stream_chat_flutter/src/commands_overlay.dart'; import 'package:stream_chat_flutter/src/emoji/emoji.dart'; diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 39023c85..091f8ae4 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -1,4 +1,6 @@ export 'package:jiffy/jiffy.dart'; +export 'package:photo_manager/photo_manager.dart' + show ThumbnailSize, ThumbnailFormat; export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; export 'src/attachment/attachment.dart'; @@ -22,6 +24,7 @@ export 'src/info_tile.dart'; export 'src/localization/stream_chat_localizations.dart'; export 'src/localization/translations.dart' show DefaultTranslations; export 'src/message_action.dart'; + // ignore: deprecated_member_use_from_same_package export 'src/message_input.dart' show MessageInput, MessageInputState; export 'src/message_list_view.dart'; @@ -49,6 +52,7 @@ export 'src/user_item.dart'; export 'src/user_list_view.dart'; export 'src/user_mention_tile.dart'; export 'src/utils.dart'; + // v4 export 'src/v4/message_input/countdown_button.dart'; export 'src/v4/message_input/stream_attachment_picker.dart'; From 6c78ad49c894d76c0b0254f36634c4343b642d9a Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 19 Aug 2022 13:01:25 +0530 Subject: [PATCH 33/49] chore(ui): fix analysis issues Signed-off-by: xsahil03x --- .../lib/src/attachment/file_attachment.dart | 3 ++- packages/stream_chat_flutter/lib/src/message_widget.dart | 3 ++- .../lib/src/v4/message_input/stream_message_input.dart | 1 - 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart index be1b33bb..2feeb0c2 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart @@ -100,7 +100,8 @@ class StreamFileAttachment extends StreamAttachmentWidget { borderRadius: BorderRadius.circular(8), ); - // TODO: Improve image memory. This is using the full image instead of a smaller version (thumbnail) + // TODO: Improve image memory. This is using the full image instead of a + // smaller version (thumbnail) Widget _getFileTypeImage(BuildContext context) { if (isImageAttachment) { return Material( diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 4f926cd1..71d0ab2c 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -434,7 +434,8 @@ class StreamMessageWidget extends StatefulWidget { /// Crop type of the image attachment thumbnail. /// /// Defaults to [center] - final String /*center|top|bottom|left|right*/ imageAttachmentThumbnailCropType; + final String /*center|top|bottom|left|right*/ + imageAttachmentThumbnailCropType; /// Creates a copy of [StreamMessageWidget] with /// specified attributes overridden. diff --git a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart index 9b7fe009..db1c1925 100644 --- a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart +++ b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart @@ -10,7 +10,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:photo_manager/photo_manager.dart'; import 'package:shimmer/shimmer.dart'; import 'package:stream_chat_flutter/src/commands_overlay.dart'; import 'package:stream_chat_flutter/src/emoji/emoji.dart'; From ceab9175ceeb21604a1b120fd95efa2268fde245 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 23 Aug 2022 16:01:52 +0530 Subject: [PATCH 34/49] Release v4.5.0 (#1308) * chore(llc, ui, core): bump version to v4.5.0 Signed-off-by: xsahil03x * chore(ui, core): update CHANGELOG.md Signed-off-by: xsahil03x Signed-off-by: xsahil03x --- packages/stream_chat/CHANGELOG.md | 2 +- packages/stream_chat/lib/version.dart | 2 +- packages/stream_chat/pubspec.yaml | 2 +- packages/stream_chat_flutter/CHANGELOG.md | 35 +++++++++++-------- packages/stream_chat_flutter/pubspec.yaml | 4 +-- .../stream_chat_flutter_core/CHANGELOG.md | 4 ++- .../stream_chat_flutter_core/pubspec.yaml | 4 +-- 7 files changed, 30 insertions(+), 23 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index d04b7ec1..6b73d0ab 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,4 +1,4 @@ -## Upcoming +## 4.5.0 🐞 Fixed diff --git a/packages/stream_chat/lib/version.dart b/packages/stream_chat/lib/version.dart index 3deb7f2f..5d9e926b 100644 --- a/packages/stream_chat/lib/version.dart +++ b/packages/stream_chat/lib/version.dart @@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart'; /// Current package version /// Used in [StreamChatClient] to build the `x-stream-client` header // ignore: constant_identifier_names -const PACKAGE_VERSION = '4.4.1'; +const PACKAGE_VERSION = '4.5.0'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index 8b15b993..c3d14d9c 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat homepage: https://getstream.io/ description: The official Dart client for Stream Chat, a service for building chat applications. -version: 4.4.1 +version: 4.5.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 4482e3ac..54fc55bd 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,4 +1,7 @@ -## Upcoming +## 4.5.0 + +- Updated `stream_chat_flutter_core` dependency + to [`4.5.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog). 🐞 Fixed @@ -14,8 +17,22 @@ ✅ Added - Added `thumbnailSize`, `thumbnailResizeType`, and `thumbnailCropType` params - to `StreamMessageWidget` and `StreamAttachmentPicker` to customize the appearance of image - thumbnails. + to `StreamMessageWidget` to customize the appearance of image attachment thumbnails. + + ```dart + StreamMessageListView( + messageBuilder: (context, details, messages, defaultMessage) { + return defaultMessage.copyWith( + imageAttachmentThumbnailSize: ..., + imageAttachmentThumbnailCropType: ..., + imageAttachmentThumbnailResizeType: ..., + ); + }, + ), + ``` + +- Added `thumbnailSize`, `thumbnailFormat`, `thumbnailQuality` and `thumbnailScale` params + to `StreamAttachmentPicker` to customize the appearance of image attachment thumbnails. ```dart StreamMessageInput( @@ -32,18 +49,6 @@ ), ``` - ```dart - StreamMessageListView( - messageBuilder: (context, details, messages, defaultMessage) { - return defaultMessage.copyWith( - imageAttachmentThumbnailSize: ..., - imageAttachmentThumbnailCropType: ..., - imageAttachmentThumbnailResizeType: ..., - ); - }, - ), - ``` - ## 4.4.1 🐞 Fixed diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 5caa7614..3d6a37c7 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter. -version: 4.4.1 +version: 4.5.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -36,7 +36,7 @@ dependencies: rxdart: ^0.27.0 share_plus: ^4.0.1 shimmer: ^2.0.0 - stream_chat_flutter_core: ^4.4.1 + stream_chat_flutter_core: ^4.5.0 substring_highlight: ^1.0.26 url_launcher: ^6.1.0 video_player: ^2.1.0 diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index 346059b4..d4288b9a 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,4 +1,6 @@ -## Upcoming +## 4.5.0 + +- Updated `stream_chat` dependency to [`4.5.0`](https://pub.dev/packages/stream_chat/changelog). 🐞 Fixed diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index be0171ec..989245c6 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter_core homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter. -version: 4.4.1 +version: 4.5.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -17,7 +17,7 @@ dependencies: freezed_annotation: ^2.0.3 meta: ^1.3.0 rxdart: ^0.27.0 - stream_chat: ^4.4.1 + stream_chat: ^4.5.0 dev_dependencies: build_runner: ^2.0.1 From c9281e0c89b6ed3f5188c64fa863da1cf41076e0 Mon Sep 17 00:00:00 2001 From: Sacha Arbonel Date: Thu, 4 Aug 2022 16:07:13 -0400 Subject: [PATCH 35/49] third party video integration --- melos.yaml | 4 +-- .../stream_chat/lib/src/client/client.dart | 13 +++++++ .../lib/src/core/api/call_api.dart | 28 +++++++++++++++ .../lib/src/core/api/stream_chat_api.dart | 6 ++++ .../lib/src/core/models/agora_payload.dart | 13 +++++++ .../lib/src/core/models/agora_payload.g.dart | 16 +++++++++ .../lib/src/core/models/call_payload.dart | 22 ++++++++++++ .../lib/src/core/models/call_payload.g.dart | 35 +++++++++++++++++++ .../src/core/models/call_token_payload.dart | 20 +++++++++++ .../src/core/models/call_token_payload.g.dart | 30 ++++++++++++++++ .../src/core/models/create_call_payload.dart | 18 ++++++++++ .../core/models/create_call_payload.g.dart | 17 +++++++++ .../lib/src/core/models/hms_payload.dart | 17 +++++++++ .../lib/src/core/models/hms_payload.g.dart | 18 ++++++++++ 14 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 packages/stream_chat/lib/src/core/api/call_api.dart create mode 100644 packages/stream_chat/lib/src/core/models/agora_payload.dart create mode 100644 packages/stream_chat/lib/src/core/models/agora_payload.g.dart create mode 100644 packages/stream_chat/lib/src/core/models/call_payload.dart create mode 100644 packages/stream_chat/lib/src/core/models/call_payload.g.dart create mode 100644 packages/stream_chat/lib/src/core/models/call_token_payload.dart create mode 100644 packages/stream_chat/lib/src/core/models/call_token_payload.g.dart create mode 100644 packages/stream_chat/lib/src/core/models/create_call_payload.dart create mode 100644 packages/stream_chat/lib/src/core/models/create_call_payload.g.dart create mode 100644 packages/stream_chat/lib/src/core/models/hms_payload.dart create mode 100644 packages/stream_chat/lib/src/core/models/hms_payload.g.dart diff --git a/melos.yaml b/melos.yaml index 10e7ca7b..3291c18e 100644 --- a/melos.yaml +++ b/melos.yaml @@ -54,11 +54,11 @@ scripts: description: Build all generated files for Dart & Flutter packages in this project. generate:dart: - run: melos exec -c 1 --depends-on="build_runner" --no-flutter -- "dart run build_runner build --delete-conflicting-outputs" + run: melos exec -c 1 --depends-on="build_runner" --no-flutter -- "dart pub run build_runner build --delete-conflicting-outputs --enable-experiment=super-parameters,enhanced-enums" description: Build all generated files for Dart packages in this project. generate:flutter: - run: melos exec -c 1 --depends-on="build_runner" --flutter -- "flutter pub run build_runner build --delete-conflicting-outputs" + run: melos exec -c 1 --depends-on="build_runner" --flutter -- "flutter pub run build_runner build --delete-conflicting-outputs --enable-experiment=super-parameters,enhanced-enums" description: Build all generated files for Flutter packages in this project. test:all: diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index c2950377..4b0482a3 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -17,8 +17,10 @@ import 'package:stream_chat/src/core/http/stream_http_client.dart'; import 'package:stream_chat/src/core/http/token.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; import 'package:stream_chat/src/core/models/attachment_file.dart'; +import 'package:stream_chat/src/core/models/call_token_payload.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; import 'package:stream_chat/src/core/models/channel_state.dart'; +import 'package:stream_chat/src/core/models/create_call_payload.dart'; import 'package:stream_chat/src/core/models/event.dart'; import 'package:stream_chat/src/core/models/filter.dart'; import 'package:stream_chat/src/core/models/member.dart'; @@ -585,6 +587,17 @@ class StreamChatClient { } } + Future getCallToken(String callId) async => + _chatApi.call.getCallToken(callId); + + Future createCall( + String callId, + String callType, + Map? options, + ) async { + return _chatApi.call.createCall(callId, callType, options); + } + /// Requests channels with a given query from the API. Future> queryChannelsOnline({ Filter? filter, diff --git a/packages/stream_chat/lib/src/core/api/call_api.dart b/packages/stream_chat/lib/src/core/api/call_api.dart new file mode 100644 index 00000000..e2f767f5 --- /dev/null +++ b/packages/stream_chat/lib/src/core/api/call_api.dart @@ -0,0 +1,28 @@ +import 'package:stream_chat/src/core/http/stream_http_client.dart'; +import 'package:stream_chat/src/core/models/call_token_payload.dart'; +import 'package:stream_chat/src/core/models/create_call_payload.dart'; + +class CallApi { + /// Initialize a new call api + CallApi(this._client); + + final StreamHttpClient _client; + + Future getCallToken(String callId) async { + final response = await _client.post( + '/calls/$callId', + ); + return CallTokenPayload.fromJson(response.data); + } + + Future createCall( + String callId, + String callType, + Map? options, + ) async { + final response = await _client.post( + '/channels/$callType/$callId/call', + ); + return CreateCallPayload.fromJson(response.data); + } +} diff --git a/packages/stream_chat/lib/src/core/api/stream_chat_api.dart b/packages/stream_chat/lib/src/core/api/stream_chat_api.dart index cf6c0bad..c19b81a9 100644 --- a/packages/stream_chat/lib/src/core/api/stream_chat_api.dart +++ b/packages/stream_chat/lib/src/core/api/stream_chat_api.dart @@ -1,5 +1,6 @@ import 'package:logging/logging.dart'; import 'package:stream_chat/src/core/api/attachment_file_uploader.dart'; +import 'package:stream_chat/src/core/api/call_api.dart'; import 'package:stream_chat/src/core/api/channel_api.dart'; import 'package:stream_chat/src/core/api/device_api.dart'; import 'package:stream_chat/src/core/api/general_api.dart'; @@ -53,6 +54,11 @@ class StreamChatApi { /// Api dedicated to message operations MessageApi get message => _message ??= MessageApi(_client); + CallApi? _call; + + /// Api dedicated to guest operations + CallApi get call => _call ??= CallApi(_client); + ChannelApi? _channel; /// Api dedicated to channel operations diff --git a/packages/stream_chat/lib/src/core/models/agora_payload.dart b/packages/stream_chat/lib/src/core/models/agora_payload.dart new file mode 100644 index 00000000..35bc93b7 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/agora_payload.dart @@ -0,0 +1,13 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'agora_payload.g.dart'; + +@JsonSerializable(includeIfNull: false) +class AgoraPayload { + final String channel; + const AgoraPayload({required this.channel}); + + factory AgoraPayload.fromJson(Map json) => + _$AgoraPayloadFromJson(json); +} diff --git a/packages/stream_chat/lib/src/core/models/agora_payload.g.dart b/packages/stream_chat/lib/src/core/models/agora_payload.g.dart new file mode 100644 index 00000000..ff4df843 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/agora_payload.g.dart @@ -0,0 +1,16 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'agora_payload.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AgoraPayload _$AgoraPayloadFromJson(Map json) => AgoraPayload( + channel: json['channel'] as String, + ); + +Map _$AgoraPayloadToJson(AgoraPayload instance) => + { + 'channel': instance.channel, + }; diff --git a/packages/stream_chat/lib/src/core/models/call_payload.dart b/packages/stream_chat/lib/src/core/models/call_payload.dart new file mode 100644 index 00000000..7754e5be --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/call_payload.dart @@ -0,0 +1,22 @@ +import 'package:stream_chat/src/core/models/agora_payload.dart'; +import 'package:stream_chat/src/core/models/hms_payload.dart'; +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'call_payload.g.dart'; + +@JsonSerializable(includeIfNull: false) +class CallPayload extends Equatable { + final String id; + final String provider; + final AgoraPayload? agora; + final HMSPayload? hms; + + const CallPayload({required this.id, required this.provider, this.agora, this.hms}); + + factory CallPayload.fromJson(Map json) => + _$CallPayloadFromJson(json); + + @override + List get props => [id, provider, agora, hms]; +} diff --git a/packages/stream_chat/lib/src/core/models/call_payload.g.dart b/packages/stream_chat/lib/src/core/models/call_payload.g.dart new file mode 100644 index 00000000..f23cfb45 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/call_payload.g.dart @@ -0,0 +1,35 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'call_payload.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CallPayload _$CallPayloadFromJson(Map json) => CallPayload( + id: json['id'] as String, + provider: json['provider'] as String, + agora: json['agora'] == null + ? null + : AgoraPayload.fromJson(json['agora'] as Map), + hms: json['hms'] == null + ? null + : HMSPayload.fromJson(json['hms'] as Map), + ); + +Map _$CallPayloadToJson(CallPayload instance) { + final val = { + 'id': instance.id, + 'provider': instance.provider, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('agora', instance.agora?.toJson()); + writeNotNull('hms', instance.hms?.toJson()); + return val; +} diff --git a/packages/stream_chat/lib/src/core/models/call_token_payload.dart b/packages/stream_chat/lib/src/core/models/call_token_payload.dart new file mode 100644 index 00000000..520572d1 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/call_token_payload.dart @@ -0,0 +1,20 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + + + +part 'call_token_payload.g.dart'; + +@JsonSerializable(includeIfNull: false) +class CallTokenPayload extends Equatable { + final String token; + final int? agoraUid; + final String? agoraAppId; + const CallTokenPayload({required this.token, this.agoraUid, this.agoraAppId}); + + factory CallTokenPayload.fromJson(Map json) => + _$CallTokenPayloadFromJson(json); + + @override + List get props => [token, agoraAppId, agoraUid]; +} diff --git a/packages/stream_chat/lib/src/core/models/call_token_payload.g.dart b/packages/stream_chat/lib/src/core/models/call_token_payload.g.dart new file mode 100644 index 00000000..df822920 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/call_token_payload.g.dart @@ -0,0 +1,30 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'call_token_payload.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CallTokenPayload _$CallTokenPayloadFromJson(Map json) => + CallTokenPayload( + token: json['token'] as String, + agoraUid: json['agora_uid'] as int?, + agoraAppId: json['agora_app_id'] as String?, + ); + +Map _$CallTokenPayloadToJson(CallTokenPayload instance) { + final val = { + 'token': instance.token, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('agora_uid', instance.agoraUid); + writeNotNull('agora_app_id', instance.agoraAppId); + return val; +} diff --git a/packages/stream_chat/lib/src/core/models/create_call_payload.dart b/packages/stream_chat/lib/src/core/models/create_call_payload.dart new file mode 100644 index 00000000..47711e2d --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/create_call_payload.dart @@ -0,0 +1,18 @@ +import 'package:equatable/equatable.dart'; +import 'package:stream_chat/src/core/models/call_payload.dart'; +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + + +part 'create_call_payload.g.dart'; +@JsonSerializable(includeIfNull: false) +class CreateCallPayload extends Equatable { + final CallPayload call; + const CreateCallPayload({required this.call}); + + factory CreateCallPayload.fromJson(Map json) => + _$CreateCallPayloadFromJson(json); + + @override + List get props => [call]; +} diff --git a/packages/stream_chat/lib/src/core/models/create_call_payload.g.dart b/packages/stream_chat/lib/src/core/models/create_call_payload.g.dart new file mode 100644 index 00000000..3c849797 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/create_call_payload.g.dart @@ -0,0 +1,17 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'create_call_payload.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CreateCallPayload _$CreateCallPayloadFromJson(Map json) => + CreateCallPayload( + call: CallPayload.fromJson(json['call'] as Map), + ); + +Map _$CreateCallPayloadToJson(CreateCallPayload instance) => + { + 'call': instance.call.toJson(), + }; diff --git a/packages/stream_chat/lib/src/core/models/hms_payload.dart b/packages/stream_chat/lib/src/core/models/hms_payload.dart new file mode 100644 index 00000000..5d5e3e96 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/hms_payload.dart @@ -0,0 +1,17 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'hms_payload.g.dart'; + +@JsonSerializable(includeIfNull: false) +class HMSPayload extends Equatable { + final String roomId; + final String roomName; + const HMSPayload({required this.roomId, required this.roomName}); + + factory HMSPayload.fromJson(Map json) => + _$HMSPayloadFromJson(json); + + @override + List get props => [roomId, roomName]; +} diff --git a/packages/stream_chat/lib/src/core/models/hms_payload.g.dart b/packages/stream_chat/lib/src/core/models/hms_payload.g.dart new file mode 100644 index 00000000..3c55f56f --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/hms_payload.g.dart @@ -0,0 +1,18 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'hms_payload.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +HMSPayload _$HMSPayloadFromJson(Map json) => HMSPayload( + roomId: json['room_id'] as String, + roomName: json['room_name'] as String, + ); + +Map _$HMSPayloadToJson(HMSPayload instance) => + { + 'room_id': instance.roomId, + 'room_name': instance.roomName, + }; From 570e6e05cfc3cc768a797ef1ef4324fe22666ded Mon Sep 17 00:00:00 2001 From: Sacha Arbonel Date: Thu, 11 Aug 2022 11:33:52 -0400 Subject: [PATCH 36/49] fix llc --- melos.yaml | 4 +-- .../stream_chat/lib/src/client/client.dart | 18 ++++++++---- .../lib/src/core/api/call_api.dart | 28 +++++++++++++------ .../lib/src/core/models/agora_payload.dart | 4 ++- .../lib/src/core/models/call_payload.dart | 7 +++-- .../lib/src/core/models/call_payload.g.dart | 23 +++++---------- .../src/core/models/call_token_payload.dart | 5 +++- .../src/core/models/call_token_payload.g.dart | 21 ++++---------- .../src/core/models/create_call_payload.dart | 6 ++-- .../lib/src/core/models/hms_payload.dart | 4 ++- packages/stream_chat/lib/stream_chat.dart | 6 ++++ 11 files changed, 72 insertions(+), 54 deletions(-) diff --git a/melos.yaml b/melos.yaml index 3291c18e..fbbfcb78 100644 --- a/melos.yaml +++ b/melos.yaml @@ -54,11 +54,11 @@ scripts: description: Build all generated files for Dart & Flutter packages in this project. generate:dart: - run: melos exec -c 1 --depends-on="build_runner" --no-flutter -- "dart pub run build_runner build --delete-conflicting-outputs --enable-experiment=super-parameters,enhanced-enums" + run: melos exec -c 1 --depends-on="build_runner" --no-flutter -- "dart run build_runner build --delete-conflicting-outputs --enable-experiment=super-parameters,enhanced-enums" description: Build all generated files for Dart packages in this project. generate:flutter: - run: melos exec -c 1 --depends-on="build_runner" --flutter -- "flutter pub run build_runner build --delete-conflicting-outputs --enable-experiment=super-parameters,enhanced-enums" + run: melos exec -c 1 --depends-on="build_runner" --flutter -- "flutter run build_runner build --delete-conflicting-outputs --enable-experiment=super-parameters,enhanced-enums" description: Build all generated files for Flutter packages in this project. test:all: diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 4b0482a3..ea221701 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -590,12 +590,20 @@ class StreamChatClient { Future getCallToken(String callId) async => _chatApi.call.getCallToken(callId); - Future createCall( - String callId, - String callType, + Future createCall({ + required String callId, + required String callType, + required String channelType, + required String channelId, Map? options, - ) async { - return _chatApi.call.createCall(callId, callType, options); + }) async { + return _chatApi.call.createCall( + callId: callId, + callType: callType, + options: options, + channelType: channelType, + channelId: channelId, + ); } /// Requests channels with a given query from the API. diff --git a/packages/stream_chat/lib/src/core/api/call_api.dart b/packages/stream_chat/lib/src/core/api/call_api.dart index e2f767f5..6c542848 100644 --- a/packages/stream_chat/lib/src/core/api/call_api.dart +++ b/packages/stream_chat/lib/src/core/api/call_api.dart @@ -11,18 +11,28 @@ class CallApi { Future getCallToken(String callId) async { final response = await _client.post( '/calls/$callId', + data: {}, ); - return CallTokenPayload.fromJson(response.data); + // return response.data; + return CallTokenPayload.fromJson(response.data); } - Future createCall( - String callId, - String callType, + Future createCall({ + required String callId, + required String callType, + required String channelType, + required String channelId, Map? options, - ) async { - final response = await _client.post( - '/channels/$callType/$callId/call', - ); - return CreateCallPayload.fromJson(response.data); + }) async { + final response = + await _client.post(_getChannelUrl(channelId, channelType), data: { + 'id': '$callId', + 'type': '$callType', + }); + // return response.data; + return CreateCallPayload.fromJson(response.data); } + + String _getChannelUrl(String channelId, String channelType) => + '/channels/$channelType/$channelId/call'; } diff --git a/packages/stream_chat/lib/src/core/models/agora_payload.dart b/packages/stream_chat/lib/src/core/models/agora_payload.dart index 35bc93b7..c70ffe0b 100644 --- a/packages/stream_chat/lib/src/core/models/agora_payload.dart +++ b/packages/stream_chat/lib/src/core/models/agora_payload.dart @@ -3,11 +3,13 @@ import 'package:json_annotation/json_annotation.dart'; part 'agora_payload.g.dart'; -@JsonSerializable(includeIfNull: false) +@JsonSerializable() class AgoraPayload { final String channel; const AgoraPayload({required this.channel}); factory AgoraPayload.fromJson(Map json) => _$AgoraPayloadFromJson(json); + + Map toJson() => _$AgoraPayloadToJson(this); } diff --git a/packages/stream_chat/lib/src/core/models/call_payload.dart b/packages/stream_chat/lib/src/core/models/call_payload.dart index 7754e5be..1da6e415 100644 --- a/packages/stream_chat/lib/src/core/models/call_payload.dart +++ b/packages/stream_chat/lib/src/core/models/call_payload.dart @@ -5,18 +5,21 @@ import 'package:json_annotation/json_annotation.dart'; part 'call_payload.g.dart'; -@JsonSerializable(includeIfNull: false) +@JsonSerializable() class CallPayload extends Equatable { final String id; final String provider; final AgoraPayload? agora; final HMSPayload? hms; - const CallPayload({required this.id, required this.provider, this.agora, this.hms}); + const CallPayload( + {required this.id, required this.provider, this.agora, this.hms}); factory CallPayload.fromJson(Map json) => _$CallPayloadFromJson(json); + Map toJson() => _$CallPayloadToJson(this); + @override List get props => [id, provider, agora, hms]; } diff --git a/packages/stream_chat/lib/src/core/models/call_payload.g.dart b/packages/stream_chat/lib/src/core/models/call_payload.g.dart index f23cfb45..239a68bf 100644 --- a/packages/stream_chat/lib/src/core/models/call_payload.g.dart +++ b/packages/stream_chat/lib/src/core/models/call_payload.g.dart @@ -17,19 +17,10 @@ CallPayload _$CallPayloadFromJson(Map json) => CallPayload( : HMSPayload.fromJson(json['hms'] as Map), ); -Map _$CallPayloadToJson(CallPayload instance) { - final val = { - 'id': instance.id, - 'provider': instance.provider, - }; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('agora', instance.agora?.toJson()); - writeNotNull('hms', instance.hms?.toJson()); - return val; -} +Map _$CallPayloadToJson(CallPayload instance) => + { + 'id': instance.id, + 'provider': instance.provider, + 'agora': instance.agora?.toJson(), + 'hms': instance.hms?.toJson(), + }; diff --git a/packages/stream_chat/lib/src/core/models/call_token_payload.dart b/packages/stream_chat/lib/src/core/models/call_token_payload.dart index 520572d1..f944fd75 100644 --- a/packages/stream_chat/lib/src/core/models/call_token_payload.dart +++ b/packages/stream_chat/lib/src/core/models/call_token_payload.dart @@ -5,7 +5,7 @@ import 'package:json_annotation/json_annotation.dart'; part 'call_token_payload.g.dart'; -@JsonSerializable(includeIfNull: false) +@JsonSerializable() class CallTokenPayload extends Equatable { final String token; final int? agoraUid; @@ -15,6 +15,9 @@ class CallTokenPayload extends Equatable { factory CallTokenPayload.fromJson(Map json) => _$CallTokenPayloadFromJson(json); + + Map toJson() => _$CallTokenPayloadToJson(this); + @override List get props => [token, agoraAppId, agoraUid]; } diff --git a/packages/stream_chat/lib/src/core/models/call_token_payload.g.dart b/packages/stream_chat/lib/src/core/models/call_token_payload.g.dart index df822920..09ab714e 100644 --- a/packages/stream_chat/lib/src/core/models/call_token_payload.g.dart +++ b/packages/stream_chat/lib/src/core/models/call_token_payload.g.dart @@ -13,18 +13,9 @@ CallTokenPayload _$CallTokenPayloadFromJson(Map json) => agoraAppId: json['agora_app_id'] as String?, ); -Map _$CallTokenPayloadToJson(CallTokenPayload instance) { - final val = { - 'token': instance.token, - }; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('agora_uid', instance.agoraUid); - writeNotNull('agora_app_id', instance.agoraAppId); - return val; -} +Map _$CallTokenPayloadToJson(CallTokenPayload instance) => + { + 'token': instance.token, + 'agora_uid': instance.agoraUid, + 'agora_app_id': instance.agoraAppId, + }; diff --git a/packages/stream_chat/lib/src/core/models/create_call_payload.dart b/packages/stream_chat/lib/src/core/models/create_call_payload.dart index 47711e2d..5c030015 100644 --- a/packages/stream_chat/lib/src/core/models/create_call_payload.dart +++ b/packages/stream_chat/lib/src/core/models/create_call_payload.dart @@ -3,9 +3,9 @@ import 'package:stream_chat/src/core/models/call_payload.dart'; import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; - part 'create_call_payload.g.dart'; -@JsonSerializable(includeIfNull: false) + +@JsonSerializable() class CreateCallPayload extends Equatable { final CallPayload call; const CreateCallPayload({required this.call}); @@ -13,6 +13,8 @@ class CreateCallPayload extends Equatable { factory CreateCallPayload.fromJson(Map json) => _$CreateCallPayloadFromJson(json); + Map toJson() => _$CreateCallPayloadToJson(this); + @override List get props => [call]; } diff --git a/packages/stream_chat/lib/src/core/models/hms_payload.dart b/packages/stream_chat/lib/src/core/models/hms_payload.dart index 5d5e3e96..1e77b475 100644 --- a/packages/stream_chat/lib/src/core/models/hms_payload.dart +++ b/packages/stream_chat/lib/src/core/models/hms_payload.dart @@ -3,7 +3,7 @@ import 'package:json_annotation/json_annotation.dart'; part 'hms_payload.g.dart'; -@JsonSerializable(includeIfNull: false) +@JsonSerializable() class HMSPayload extends Equatable { final String roomId; final String roomName; @@ -12,6 +12,8 @@ class HMSPayload extends Equatable { factory HMSPayload.fromJson(Map json) => _$HMSPayloadFromJson(json); + Map toJson() => _$HMSPayloadToJson(this); + @override List get props => [roomId, roomName]; } diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 7f4c22ef..62137352 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -15,15 +15,21 @@ export './src/core/api/responses.dart'; export './src/core/api/stream_chat_api.dart' show PushProvider; export './src/core/error/error.dart'; export './src/core/models/action.dart'; +export './src/core/models/agora_payload.dart'; export './src/core/models/attachment.dart'; export './src/core/models/attachment_file.dart'; +export './src/core/models/call_payload.dart'; +export './src/core/models/call_token_payload.dart'; export './src/core/models/channel_config.dart'; export './src/core/models/channel_model.dart'; export './src/core/models/channel_state.dart'; export './src/core/models/command.dart'; +export './src/core/models/create_call_payload.dart'; export './src/core/models/device.dart'; export './src/core/models/event.dart'; export './src/core/models/filter.dart' show Filter; +export './src/core/models/hms_payload.dart'; +export './src/core/models/hms_payload.dart'; export './src/core/models/member.dart'; export './src/core/models/message.dart'; export './src/core/models/mute.dart'; From a5d0c74fd644076437feff635bcfaba43751c8c0 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 25 Aug 2022 12:30:50 +0200 Subject: [PATCH 37/49] refactor: clean up and add docs --- .../stream_chat/lib/src/client/client.dart | 4 +- .../lib/src/core/api/call_api.dart | 10 +- .../lib/src/core/api/responses.dart | 122 ++++++++++++++++++ .../lib/src/core/api/responses.g.dart | 63 +++++++++ .../lib/src/core/models/agora_payload.dart | 15 --- .../lib/src/core/models/agora_payload.g.dart | 16 --- .../lib/src/core/models/call_payload.dart | 25 ---- .../lib/src/core/models/call_payload.g.dart | 26 ---- .../src/core/models/call_token_payload.dart | 23 ---- .../src/core/models/call_token_payload.g.dart | 21 --- .../src/core/models/create_call_payload.dart | 20 --- .../core/models/create_call_payload.g.dart | 17 --- .../lib/src/core/models/hms_payload.dart | 19 --- .../lib/src/core/models/hms_payload.g.dart | 18 --- packages/stream_chat/lib/stream_chat.dart | 6 - 15 files changed, 193 insertions(+), 212 deletions(-) delete mode 100644 packages/stream_chat/lib/src/core/models/agora_payload.dart delete mode 100644 packages/stream_chat/lib/src/core/models/agora_payload.g.dart delete mode 100644 packages/stream_chat/lib/src/core/models/call_payload.dart delete mode 100644 packages/stream_chat/lib/src/core/models/call_payload.g.dart delete mode 100644 packages/stream_chat/lib/src/core/models/call_token_payload.dart delete mode 100644 packages/stream_chat/lib/src/core/models/call_token_payload.g.dart delete mode 100644 packages/stream_chat/lib/src/core/models/create_call_payload.dart delete mode 100644 packages/stream_chat/lib/src/core/models/create_call_payload.g.dart delete mode 100644 packages/stream_chat/lib/src/core/models/hms_payload.dart delete mode 100644 packages/stream_chat/lib/src/core/models/hms_payload.g.dart diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index ea221701..0654f837 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -17,10 +17,8 @@ import 'package:stream_chat/src/core/http/stream_http_client.dart'; import 'package:stream_chat/src/core/http/token.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; import 'package:stream_chat/src/core/models/attachment_file.dart'; -import 'package:stream_chat/src/core/models/call_token_payload.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; import 'package:stream_chat/src/core/models/channel_state.dart'; -import 'package:stream_chat/src/core/models/create_call_payload.dart'; import 'package:stream_chat/src/core/models/event.dart'; import 'package:stream_chat/src/core/models/filter.dart'; import 'package:stream_chat/src/core/models/member.dart'; @@ -587,9 +585,11 @@ class StreamChatClient { } } + /// Returns a token associated with the [callId]. Future getCallToken(String callId) async => _chatApi.call.getCallToken(callId); + /// Creates a new call. Future createCall({ required String callId, required String callType, diff --git a/packages/stream_chat/lib/src/core/api/call_api.dart b/packages/stream_chat/lib/src/core/api/call_api.dart index 6c542848..d7deff1e 100644 --- a/packages/stream_chat/lib/src/core/api/call_api.dart +++ b/packages/stream_chat/lib/src/core/api/call_api.dart @@ -1,13 +1,14 @@ +import 'package:stream_chat/src/core/api/responses.dart'; import 'package:stream_chat/src/core/http/stream_http_client.dart'; -import 'package:stream_chat/src/core/models/call_token_payload.dart'; -import 'package:stream_chat/src/core/models/create_call_payload.dart'; +/// Defines the api dedicated to video call operations class CallApi { /// Initialize a new call api CallApi(this._client); final StreamHttpClient _client; + /// Returns a token dedicated to the [callId] Future getCallToken(String callId) async { final response = await _client.post( '/calls/$callId', @@ -17,6 +18,7 @@ class CallApi { return CallTokenPayload.fromJson(response.data); } + /// Creates a new call Future createCall({ required String callId, required String callType, @@ -26,8 +28,8 @@ class CallApi { }) async { final response = await _client.post(_getChannelUrl(channelId, channelType), data: { - 'id': '$callId', - 'type': '$callType', + 'id': callId, + 'type': callType, }); // return response.data; return CreateCallPayload.fromJson(response.data); diff --git a/packages/stream_chat/lib/src/core/api/responses.dart b/packages/stream_chat/lib/src/core/api/responses.dart index d608c5aa..72142778 100644 --- a/packages/stream_chat/lib/src/core/api/responses.dart +++ b/packages/stream_chat/lib/src/core/api/responses.dart @@ -1,5 +1,7 @@ +import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/client/client.dart'; +import 'package:stream_chat/src/core/api/call_api.dart'; import 'package:stream_chat/src/core/error/error.dart'; import 'package:stream_chat/src/core/models/banned_user.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; @@ -495,3 +497,123 @@ class OGAttachmentResponse extends _BaseResponse { static OGAttachmentResponse fromJson(Map json) => _$OGAttachmentResponseFromJson(json); } + +/// Payload for Agora call. +@JsonSerializable() +class AgoraPayload { + /// Create a new instance. + const AgoraPayload({required this.channel}); + + /// Create a new instance from a [json]. + factory AgoraPayload.fromJson(Map json) => + _$AgoraPayloadFromJson(json); + + /// The Agora channel. + final String channel; + + /// Convert this object to a json. + Map toJson() => _$AgoraPayloadToJson(this); +} + +/// Model containing the information about a call. +@JsonSerializable() +class CallPayload extends Equatable { + /// Create a new instance. + const CallPayload({ + required this.id, + required this.provider, + this.agora, + this.hms, + }); + + /// Create a new instance from a [json]. + factory CallPayload.fromJson(Map json) => + _$CallPayloadFromJson(json); + + /// The call id. + final String id; + + /// The call provider. + final String provider; + + /// The payload specific to Agora. + final AgoraPayload? agora; + + /// The payload specific to 100ms. + final HMSPayload? hms; + + /// Convert this object to a json. + Map toJson() => _$CallPayloadToJson(this); + + @override + List get props => [id, provider, agora, hms]; +} + +/// The response to [CallApi.getCallToken] +@JsonSerializable() +class CallTokenPayload extends Equatable { + /// Create a new instance. + const CallTokenPayload({required this.token, this.agoraUid, this.agoraAppId}); + + /// Create a new instance from a [json]. + factory CallTokenPayload.fromJson(Map json) => + _$CallTokenPayloadFromJson(json); + + /// The token to use for the call. + final String token; + + /// The uid specific to Agora. + final int? agoraUid; + + /// The appId specific to 100ms. + final String? agoraAppId; + + /// Convert this object to a json. + Map toJson() => _$CallTokenPayloadToJson(this); + + @override + List get props => [token, agoraAppId, agoraUid]; +} + +/// The response to [CallApi.createCall] +@JsonSerializable() +class CreateCallPayload extends Equatable { + /// Creates a new instance. + const CreateCallPayload({required this.call}); + + /// Create a new instance from a [json]. + factory CreateCallPayload.fromJson(Map json) => + _$CreateCallPayloadFromJson(json); + + /// The call object. + final CallPayload call; + + /// Convert this object to a json. + Map toJson() => _$CreateCallPayloadToJson(this); + + @override + List get props => [call]; +} + +/// Payload for 100ms call. +@JsonSerializable() +class HMSPayload extends Equatable { + /// Create a new instance. + const HMSPayload({required this.roomId, required this.roomName}); + + /// Create a new instance from a [json]. + factory HMSPayload.fromJson(Map json) => + _$HMSPayloadFromJson(json); + + /// The id of the 100ms room. + final String roomId; + + /// The name of the 100ms room. + final String roomName; + + /// Convert this object to a json. + Map toJson() => _$HMSPayloadToJson(this); + + @override + List get props => [roomId, roomName]; +} diff --git a/packages/stream_chat/lib/src/core/api/responses.g.dart b/packages/stream_chat/lib/src/core/api/responses.g.dart index 991b39f6..c82769c1 100644 --- a/packages/stream_chat/lib/src/core/api/responses.g.dart +++ b/packages/stream_chat/lib/src/core/api/responses.g.dart @@ -297,3 +297,66 @@ OGAttachmentResponse _$OGAttachmentResponseFromJson( ..title = json['title'] as String? ..titleLink = json['title_link'] as String? ..type = json['type'] as String?; + +AgoraPayload _$AgoraPayloadFromJson(Map json) => AgoraPayload( + channel: json['channel'] as String, + ); + +Map _$AgoraPayloadToJson(AgoraPayload instance) => + { + 'channel': instance.channel, + }; + +CallPayload _$CallPayloadFromJson(Map json) => CallPayload( + id: json['id'] as String, + provider: json['provider'] as String, + agora: json['agora'] == null + ? null + : AgoraPayload.fromJson(json['agora'] as Map), + hms: json['hms'] == null + ? null + : HMSPayload.fromJson(json['hms'] as Map), + ); + +Map _$CallPayloadToJson(CallPayload instance) => + { + 'id': instance.id, + 'provider': instance.provider, + 'agora': instance.agora?.toJson(), + 'hms': instance.hms?.toJson(), + }; + +CallTokenPayload _$CallTokenPayloadFromJson(Map json) => + CallTokenPayload( + token: json['token'] as String, + agoraUid: json['agora_uid'] as int?, + agoraAppId: json['agora_app_id'] as String?, + ); + +Map _$CallTokenPayloadToJson(CallTokenPayload instance) => + { + 'token': instance.token, + 'agora_uid': instance.agoraUid, + 'agora_app_id': instance.agoraAppId, + }; + +CreateCallPayload _$CreateCallPayloadFromJson(Map json) => + CreateCallPayload( + call: CallPayload.fromJson(json['call'] as Map), + ); + +Map _$CreateCallPayloadToJson(CreateCallPayload instance) => + { + 'call': instance.call.toJson(), + }; + +HMSPayload _$HMSPayloadFromJson(Map json) => HMSPayload( + roomId: json['room_id'] as String, + roomName: json['room_name'] as String, + ); + +Map _$HMSPayloadToJson(HMSPayload instance) => + { + 'room_id': instance.roomId, + 'room_name': instance.roomName, + }; diff --git a/packages/stream_chat/lib/src/core/models/agora_payload.dart b/packages/stream_chat/lib/src/core/models/agora_payload.dart deleted file mode 100644 index c70ffe0b..00000000 --- a/packages/stream_chat/lib/src/core/models/agora_payload.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:json_annotation/json_annotation.dart'; - -part 'agora_payload.g.dart'; - -@JsonSerializable() -class AgoraPayload { - final String channel; - const AgoraPayload({required this.channel}); - - factory AgoraPayload.fromJson(Map json) => - _$AgoraPayloadFromJson(json); - - Map toJson() => _$AgoraPayloadToJson(this); -} diff --git a/packages/stream_chat/lib/src/core/models/agora_payload.g.dart b/packages/stream_chat/lib/src/core/models/agora_payload.g.dart deleted file mode 100644 index ff4df843..00000000 --- a/packages/stream_chat/lib/src/core/models/agora_payload.g.dart +++ /dev/null @@ -1,16 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'agora_payload.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -AgoraPayload _$AgoraPayloadFromJson(Map json) => AgoraPayload( - channel: json['channel'] as String, - ); - -Map _$AgoraPayloadToJson(AgoraPayload instance) => - { - 'channel': instance.channel, - }; diff --git a/packages/stream_chat/lib/src/core/models/call_payload.dart b/packages/stream_chat/lib/src/core/models/call_payload.dart deleted file mode 100644 index 1da6e415..00000000 --- a/packages/stream_chat/lib/src/core/models/call_payload.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:stream_chat/src/core/models/agora_payload.dart'; -import 'package:stream_chat/src/core/models/hms_payload.dart'; -import 'package:equatable/equatable.dart'; -import 'package:json_annotation/json_annotation.dart'; - -part 'call_payload.g.dart'; - -@JsonSerializable() -class CallPayload extends Equatable { - final String id; - final String provider; - final AgoraPayload? agora; - final HMSPayload? hms; - - const CallPayload( - {required this.id, required this.provider, this.agora, this.hms}); - - factory CallPayload.fromJson(Map json) => - _$CallPayloadFromJson(json); - - Map toJson() => _$CallPayloadToJson(this); - - @override - List get props => [id, provider, agora, hms]; -} diff --git a/packages/stream_chat/lib/src/core/models/call_payload.g.dart b/packages/stream_chat/lib/src/core/models/call_payload.g.dart deleted file mode 100644 index 239a68bf..00000000 --- a/packages/stream_chat/lib/src/core/models/call_payload.g.dart +++ /dev/null @@ -1,26 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'call_payload.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -CallPayload _$CallPayloadFromJson(Map json) => CallPayload( - id: json['id'] as String, - provider: json['provider'] as String, - agora: json['agora'] == null - ? null - : AgoraPayload.fromJson(json['agora'] as Map), - hms: json['hms'] == null - ? null - : HMSPayload.fromJson(json['hms'] as Map), - ); - -Map _$CallPayloadToJson(CallPayload instance) => - { - 'id': instance.id, - 'provider': instance.provider, - 'agora': instance.agora?.toJson(), - 'hms': instance.hms?.toJson(), - }; diff --git a/packages/stream_chat/lib/src/core/models/call_token_payload.dart b/packages/stream_chat/lib/src/core/models/call_token_payload.dart deleted file mode 100644 index f944fd75..00000000 --- a/packages/stream_chat/lib/src/core/models/call_token_payload.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:json_annotation/json_annotation.dart'; - - - -part 'call_token_payload.g.dart'; - -@JsonSerializable() -class CallTokenPayload extends Equatable { - final String token; - final int? agoraUid; - final String? agoraAppId; - const CallTokenPayload({required this.token, this.agoraUid, this.agoraAppId}); - - factory CallTokenPayload.fromJson(Map json) => - _$CallTokenPayloadFromJson(json); - - - Map toJson() => _$CallTokenPayloadToJson(this); - - @override - List get props => [token, agoraAppId, agoraUid]; -} diff --git a/packages/stream_chat/lib/src/core/models/call_token_payload.g.dart b/packages/stream_chat/lib/src/core/models/call_token_payload.g.dart deleted file mode 100644 index 09ab714e..00000000 --- a/packages/stream_chat/lib/src/core/models/call_token_payload.g.dart +++ /dev/null @@ -1,21 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'call_token_payload.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -CallTokenPayload _$CallTokenPayloadFromJson(Map json) => - CallTokenPayload( - token: json['token'] as String, - agoraUid: json['agora_uid'] as int?, - agoraAppId: json['agora_app_id'] as String?, - ); - -Map _$CallTokenPayloadToJson(CallTokenPayload instance) => - { - 'token': instance.token, - 'agora_uid': instance.agoraUid, - 'agora_app_id': instance.agoraAppId, - }; diff --git a/packages/stream_chat/lib/src/core/models/create_call_payload.dart b/packages/stream_chat/lib/src/core/models/create_call_payload.dart deleted file mode 100644 index 5c030015..00000000 --- a/packages/stream_chat/lib/src/core/models/create_call_payload.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:stream_chat/src/core/models/call_payload.dart'; -import 'package:equatable/equatable.dart'; -import 'package:json_annotation/json_annotation.dart'; - -part 'create_call_payload.g.dart'; - -@JsonSerializable() -class CreateCallPayload extends Equatable { - final CallPayload call; - const CreateCallPayload({required this.call}); - - factory CreateCallPayload.fromJson(Map json) => - _$CreateCallPayloadFromJson(json); - - Map toJson() => _$CreateCallPayloadToJson(this); - - @override - List get props => [call]; -} diff --git a/packages/stream_chat/lib/src/core/models/create_call_payload.g.dart b/packages/stream_chat/lib/src/core/models/create_call_payload.g.dart deleted file mode 100644 index 3c849797..00000000 --- a/packages/stream_chat/lib/src/core/models/create_call_payload.g.dart +++ /dev/null @@ -1,17 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'create_call_payload.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -CreateCallPayload _$CreateCallPayloadFromJson(Map json) => - CreateCallPayload( - call: CallPayload.fromJson(json['call'] as Map), - ); - -Map _$CreateCallPayloadToJson(CreateCallPayload instance) => - { - 'call': instance.call.toJson(), - }; diff --git a/packages/stream_chat/lib/src/core/models/hms_payload.dart b/packages/stream_chat/lib/src/core/models/hms_payload.dart deleted file mode 100644 index 1e77b475..00000000 --- a/packages/stream_chat/lib/src/core/models/hms_payload.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:json_annotation/json_annotation.dart'; - -part 'hms_payload.g.dart'; - -@JsonSerializable() -class HMSPayload extends Equatable { - final String roomId; - final String roomName; - const HMSPayload({required this.roomId, required this.roomName}); - - factory HMSPayload.fromJson(Map json) => - _$HMSPayloadFromJson(json); - - Map toJson() => _$HMSPayloadToJson(this); - - @override - List get props => [roomId, roomName]; -} diff --git a/packages/stream_chat/lib/src/core/models/hms_payload.g.dart b/packages/stream_chat/lib/src/core/models/hms_payload.g.dart deleted file mode 100644 index 3c55f56f..00000000 --- a/packages/stream_chat/lib/src/core/models/hms_payload.g.dart +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'hms_payload.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -HMSPayload _$HMSPayloadFromJson(Map json) => HMSPayload( - roomId: json['room_id'] as String, - roomName: json['room_name'] as String, - ); - -Map _$HMSPayloadToJson(HMSPayload instance) => - { - 'room_id': instance.roomId, - 'room_name': instance.roomName, - }; diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 62137352..7f4c22ef 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -15,21 +15,15 @@ export './src/core/api/responses.dart'; export './src/core/api/stream_chat_api.dart' show PushProvider; export './src/core/error/error.dart'; export './src/core/models/action.dart'; -export './src/core/models/agora_payload.dart'; export './src/core/models/attachment.dart'; export './src/core/models/attachment_file.dart'; -export './src/core/models/call_payload.dart'; -export './src/core/models/call_token_payload.dart'; export './src/core/models/channel_config.dart'; export './src/core/models/channel_model.dart'; export './src/core/models/channel_state.dart'; export './src/core/models/command.dart'; -export './src/core/models/create_call_payload.dart'; export './src/core/models/device.dart'; export './src/core/models/event.dart'; export './src/core/models/filter.dart' show Filter; -export './src/core/models/hms_payload.dart'; -export './src/core/models/hms_payload.dart'; export './src/core/models/member.dart'; export './src/core/models/message.dart'; export './src/core/models/mute.dart'; From ba13c9b02f59ca99635d1e43564e8652291ee0a5 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 25 Aug 2022 12:39:41 +0200 Subject: [PATCH 38/49] chore(llc): update changelog --- packages/stream_chat/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 6b73d0ab..de35d0a6 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +✅ Added + +- Added `StreamChatClient.getCallToken` and `StreamChatClient.createCall` methods. + ## 4.5.0 🐞 Fixed From af055e367b4028de30178095fd6eeac53fb2f53a Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 25 Aug 2022 14:10:22 +0200 Subject: [PATCH 39/49] chore(llc): add tests --- .../lib/src/core/api/responses.dart | 57 +++++------------- .../lib/src/core/api/responses.g.dart | 49 ++++------------ .../test/src/core/api/responses_test.dart | 58 +++++++++++++++++++ 3 files changed, 83 insertions(+), 81 deletions(-) diff --git a/packages/stream_chat/lib/src/core/api/responses.dart b/packages/stream_chat/lib/src/core/api/responses.dart index 72142778..ddeb4b0f 100644 --- a/packages/stream_chat/lib/src/core/api/responses.dart +++ b/packages/stream_chat/lib/src/core/api/responses.dart @@ -499,7 +499,7 @@ class OGAttachmentResponse extends _BaseResponse { } /// Payload for Agora call. -@JsonSerializable() +@JsonSerializable(createToJson: false) class AgoraPayload { /// Create a new instance. const AgoraPayload({required this.channel}); @@ -510,13 +510,10 @@ class AgoraPayload { /// The Agora channel. final String channel; - - /// Convert this object to a json. - Map toJson() => _$AgoraPayloadToJson(this); } /// Model containing the information about a call. -@JsonSerializable() +@JsonSerializable(createToJson: false) class CallPayload extends Equatable { /// Create a new instance. const CallPayload({ @@ -542,61 +539,40 @@ class CallPayload extends Equatable { /// The payload specific to 100ms. final HMSPayload? hms; - /// Convert this object to a json. - Map toJson() => _$CallPayloadToJson(this); - @override List get props => [id, provider, agora, hms]; } /// The response to [CallApi.getCallToken] -@JsonSerializable() -class CallTokenPayload extends Equatable { - /// Create a new instance. - const CallTokenPayload({required this.token, this.agoraUid, this.agoraAppId}); - +@JsonSerializable(createToJson: false) +class CallTokenPayload extends _BaseResponse { /// Create a new instance from a [json]. - factory CallTokenPayload.fromJson(Map json) => + static CallTokenPayload fromJson(Map json) => _$CallTokenPayloadFromJson(json); /// The token to use for the call. - final String token; + String? token; - /// The uid specific to Agora. - final int? agoraUid; + /// The user id specific to Agora. + int? agoraUid; - /// The appId specific to 100ms. - final String? agoraAppId; - - /// Convert this object to a json. - Map toJson() => _$CallTokenPayloadToJson(this); - - @override - List get props => [token, agoraAppId, agoraUid]; + /// The appId specific to Agora. + String? agoraAppId; } /// The response to [CallApi.createCall] -@JsonSerializable() -class CreateCallPayload extends Equatable { - /// Creates a new instance. - const CreateCallPayload({required this.call}); - +@JsonSerializable(createToJson: false) +class CreateCallPayload extends _BaseResponse { /// Create a new instance from a [json]. - factory CreateCallPayload.fromJson(Map json) => + static CreateCallPayload fromJson(Map json) => _$CreateCallPayloadFromJson(json); /// The call object. - final CallPayload call; - - /// Convert this object to a json. - Map toJson() => _$CreateCallPayloadToJson(this); - - @override - List get props => [call]; + CallPayload? call; } /// Payload for 100ms call. -@JsonSerializable() +@JsonSerializable(createToJson: false) class HMSPayload extends Equatable { /// Create a new instance. const HMSPayload({required this.roomId, required this.roomName}); @@ -611,9 +587,6 @@ class HMSPayload extends Equatable { /// The name of the 100ms room. final String roomName; - /// Convert this object to a json. - Map toJson() => _$HMSPayloadToJson(this); - @override List get props => [roomId, roomName]; } diff --git a/packages/stream_chat/lib/src/core/api/responses.g.dart b/packages/stream_chat/lib/src/core/api/responses.g.dart index c82769c1..6cc2158e 100644 --- a/packages/stream_chat/lib/src/core/api/responses.g.dart +++ b/packages/stream_chat/lib/src/core/api/responses.g.dart @@ -302,11 +302,6 @@ AgoraPayload _$AgoraPayloadFromJson(Map json) => AgoraPayload( channel: json['channel'] as String, ); -Map _$AgoraPayloadToJson(AgoraPayload instance) => - { - 'channel': instance.channel, - }; - CallPayload _$CallPayloadFromJson(Map json) => CallPayload( id: json['id'] as String, provider: json['provider'] as String, @@ -318,45 +313,21 @@ CallPayload _$CallPayloadFromJson(Map json) => CallPayload( : HMSPayload.fromJson(json['hms'] as Map), ); -Map _$CallPayloadToJson(CallPayload instance) => - { - 'id': instance.id, - 'provider': instance.provider, - 'agora': instance.agora?.toJson(), - 'hms': instance.hms?.toJson(), - }; - CallTokenPayload _$CallTokenPayloadFromJson(Map json) => - CallTokenPayload( - token: json['token'] as String, - agoraUid: json['agora_uid'] as int?, - agoraAppId: json['agora_app_id'] as String?, - ); - -Map _$CallTokenPayloadToJson(CallTokenPayload instance) => - { - 'token': instance.token, - 'agora_uid': instance.agoraUid, - 'agora_app_id': instance.agoraAppId, - }; + CallTokenPayload() + ..duration = json['duration'] as String? + ..token = json['token'] as String? + ..agoraUid = json['agora_uid'] as int? + ..agoraAppId = json['agora_app_id'] as String?; CreateCallPayload _$CreateCallPayloadFromJson(Map json) => - CreateCallPayload( - call: CallPayload.fromJson(json['call'] as Map), - ); - -Map _$CreateCallPayloadToJson(CreateCallPayload instance) => - { - 'call': instance.call.toJson(), - }; + CreateCallPayload() + ..duration = json['duration'] as String? + ..call = json['call'] == null + ? null + : CallPayload.fromJson(json['call'] as Map); HMSPayload _$HMSPayloadFromJson(Map json) => HMSPayload( roomId: json['room_id'] as String, roomName: json['room_name'] as String, ); - -Map _$HMSPayloadToJson(HMSPayload instance) => - { - 'room_id': instance.roomId, - 'room_name': instance.roomName, - }; diff --git a/packages/stream_chat/test/src/core/api/responses_test.dart b/packages/stream_chat/test/src/core/api/responses_test.dart index 8a73fccd..75a374fb 100644 --- a/packages/stream_chat/test/src/core/api/responses_test.dart +++ b/packages/stream_chat/test/src/core/api/responses_test.dart @@ -4340,5 +4340,63 @@ void main() { expect(response.members, isA>()); expect(response.message, isA()); }); + + test('AgoraPayload', () { + const jsonExample = ''' + {"channel":"test"} + '''; + final response = AgoraPayload.fromJson(json.decode(jsonExample)); + expect(response.channel, isA()); + }); + + test('CallPayload', () { + const jsonExample = ''' + {"id":"test", + "provider": "test", + "agora": {"channel":"test"}, + "hms":{"room_id":"test", "room_name":"test"} + } + '''; + final response = CallPayload.fromJson(json.decode(jsonExample)); + expect(response.agora, isA()); + expect(response.hms, isA()); + expect(response.id, isA()); + expect(response.provider, isA()); + }); + }); + + test('CallTokenPayload', () { + const jsonExample = ''' + {"duration": "3ms", + "agora_app_id":"test", + "agora_uid": 12, + "token": "token"} + '''; + final response = CallTokenPayload.fromJson(json.decode(jsonExample)); + expect(response.agoraAppId, isA()); + expect(response.agoraUid, isA()); + expect(response.token, isA()); + }); + + test('CreateCallPayload', () { + const jsonExample = ''' + {"call": + {"id":"test", + "provider": "test", + "agora": {"channel":"test"}, + "hms":{"room_id":"test", "room_name":"test"} + }} + '''; + final response = CreateCallPayload.fromJson(json.decode(jsonExample)); + expect(response.call, isA()); + }); + + test('HMSPayload', () { + const jsonExample = ''' + {"room_id":"test", "room_name":"test"} + '''; + final response = HMSPayload.fromJson(json.decode(jsonExample)); + expect(response.roomId, isA()); + expect(response.roomName, isA()); }); } From b924337d249d848b99caee4dfac13fd19d47fb62 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 25 Aug 2022 18:17:54 +0200 Subject: [PATCH 40/49] chore(llc): add more tests --- .../stream_chat/lib/src/client/client.dart | 2 - .../lib/src/core/api/call_api.dart | 1 - .../test/src/core/api/call_api_test.dart | 72 +++++++++++++++++++ 3 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 packages/stream_chat/test/src/core/api/call_api_test.dart diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 0654f837..73f0f95e 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -595,12 +595,10 @@ class StreamChatClient { required String callType, required String channelType, required String channelId, - Map? options, }) async { return _chatApi.call.createCall( callId: callId, callType: callType, - options: options, channelType: channelType, channelId: channelId, ); diff --git a/packages/stream_chat/lib/src/core/api/call_api.dart b/packages/stream_chat/lib/src/core/api/call_api.dart index d7deff1e..60fc6f88 100644 --- a/packages/stream_chat/lib/src/core/api/call_api.dart +++ b/packages/stream_chat/lib/src/core/api/call_api.dart @@ -24,7 +24,6 @@ class CallApi { required String callType, required String channelType, required String channelId, - Map? options, }) async { final response = await _client.post(_getChannelUrl(channelId, channelType), data: { diff --git a/packages/stream_chat/test/src/core/api/call_api_test.dart b/packages/stream_chat/test/src/core/api/call_api_test.dart new file mode 100644 index 00000000..36b173f9 --- /dev/null +++ b/packages/stream_chat/test/src/core/api/call_api_test.dart @@ -0,0 +1,72 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/api/call_api.dart'; +import 'package:stream_chat/src/core/api/device_api.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/test.dart'; + +import '../../mocks.dart'; + +void main() { + Response successResponse(String path, {Object? data}) => Response( + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); + + late final client = MockHttpClient(); + late CallApi callApi; + + setUp(() { + callApi = CallApi(client); + }); + + test('getCallToken should work', () async { + const callId = 'test-call-id'; + const path = '/calls/$callId'; + + when(() => client.post( + path, + data: {}, + )) + .thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await callApi.getCallToken(callId); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('createCall should work', () async { + const callId = 'test-call-id'; + const callType = 'test-call-type'; + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const path = '/channels/$channelType/$channelId/call'; + + when(() => client.post( + path, + data: { + 'id': callId, + 'type': callType, + }, + )) + .thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await callApi.createCall( + callId: callId, + callType: callType, + channelType: channelType, + channelId: channelId, + ); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); +} From 24b1a8087bef07b015363cd558503add6efc569e Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 26 Aug 2022 09:18:31 +0200 Subject: [PATCH 41/49] fix analysis --- packages/stream_chat/test/src/core/api/call_api_test.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/stream_chat/test/src/core/api/call_api_test.dart b/packages/stream_chat/test/src/core/api/call_api_test.dart index 36b173f9..662f84b0 100644 --- a/packages/stream_chat/test/src/core/api/call_api_test.dart +++ b/packages/stream_chat/test/src/core/api/call_api_test.dart @@ -1,8 +1,6 @@ import 'package:dio/dio.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat/src/core/api/call_api.dart'; -import 'package:stream_chat/src/core/api/device_api.dart'; -import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; import '../../mocks.dart'; From c414150bc883ff4fec081555d276d45e09027bd1 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 26 Aug 2022 09:42:03 +0200 Subject: [PATCH 42/49] chore(llc): add more tests --- .../test/src/core/api/requests_test.dart | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/packages/stream_chat/test/src/core/api/requests_test.dart b/packages/stream_chat/test/src/core/api/requests_test.dart index 876a3f61..72f97c7b 100644 --- a/packages/stream_chat/test/src/core/api/requests_test.dart +++ b/packages/stream_chat/test/src/core/api/requests_test.dart @@ -26,6 +26,53 @@ void main() { } }, ); + + test('copyWith', () { + final params = PaginationParams( + offset: 10, + limit: 20, + createdAtAfter: DateTime.now(), + createdAtAfterOrEqual: DateTime.now(), + createdAtAround: DateTime.now(), + createdAtBefore: DateTime.now(), + createdAtBeforeOrEqual: DateTime.now(), + greaterThan: 'greater-than', + greaterThanOrEqual: 'greater-than-or-equal', + lessThan: 'less-than', + lessThanOrEqual: 'less-than-or-equal', + idAround: 'id-around', + ); + + final newDateTime = DateTime.now().add(const Duration(days: 2)); + const newTestString = 'test'; + final newParams = params.copyWith( + limit: 2, + offset: 2, + createdAtAfter: newDateTime, + createdAtAfterOrEqual: newDateTime, + createdAtAround: newDateTime, + createdAtBefore: newDateTime, + createdAtBeforeOrEqual: newDateTime, + greaterThan: newTestString, + greaterThanOrEqual: newTestString, + lessThan: newTestString, + lessThanOrEqual: newTestString, + idAround: newTestString, + ); + + expect(newParams.limit, 2); + expect(newParams.offset, 2); + expect(newParams.createdAtAfter, newDateTime); + expect(newParams.createdAtAfterOrEqual, newDateTime); + expect(newParams.createdAtAround, newDateTime); + expect(newParams.createdAtBefore, newDateTime); + expect(newParams.createdAtBeforeOrEqual, newDateTime); + expect(newParams.greaterThan, newTestString); + expect(newParams.greaterThanOrEqual, newTestString); + expect(newParams.lessThan, newTestString); + expect(newParams.lessThanOrEqual, newTestString); + expect(newParams.idAround, newTestString); + }); }); }); } From 846d94d058dd9dd984849cabe7d98b6dba3668d0 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 26 Aug 2022 09:49:28 +0200 Subject: [PATCH 43/49] chore(llc): add more tests --- packages/stream_chat/test/src/core/api/requests_test.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/stream_chat/test/src/core/api/requests_test.dart b/packages/stream_chat/test/src/core/api/requests_test.dart index 72f97c7b..0bfdca7e 100644 --- a/packages/stream_chat/test/src/core/api/requests_test.dart +++ b/packages/stream_chat/test/src/core/api/requests_test.dart @@ -43,6 +43,9 @@ void main() { idAround: 'id-around', ); + final sameOld = params.copyWith(); + expect(sameOld, equals(params)); + final newDateTime = DateTime.now().add(const Duration(days: 2)); const newTestString = 'test'; final newParams = params.copyWith( From bbdf54cf249440b95150731b48005f8a6d95a65b Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 29 Aug 2022 15:18:06 +0530 Subject: [PATCH 44/49] fix(llc): only start listening to event once connected. Signed-off-by: xsahil03x --- .../stream_chat/lib/src/client/client.dart | 134 ++++++++++++------ 1 file changed, 87 insertions(+), 47 deletions(-) diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index c2950377..79443394 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -397,6 +397,10 @@ class StreamChatClient { user, includeUserDetails: includeUserDetailsInConnectCall, ); + + // Start listening to events + state.subscribeToEvents(); + return user.merge(event.me); } catch (e, stk) { logger.severe('error connecting ws', e, stk); @@ -418,6 +422,9 @@ class StreamChatClient { _connectionStatusSubscription?.cancel(); _connectionStatusSubscription = null; + // Stop listening to events + state.cancelEventSubscription(); + _ws.disconnect(); } @@ -1471,29 +1478,40 @@ class StreamChatClient { /// The class that handles the state of the channel listening to the events class ClientState { /// Creates a new instance listening to events and updating the state - ClientState(this._client) { - _subscriptions.addAll([ - _client + ClientState(this._client); + + CompositeSubscription? _eventsSubscription; + + /// Starts listening to the client events. + void subscribeToEvents() { + if (_eventsSubscription != null) { + cancelEventSubscription(); + } + + _eventsSubscription = CompositeSubscription(); + _eventsSubscription! + ..add(_client .on() .where((event) => event.me != null && event.type != EventType.healthCheck) .map((e) => e.me!) - .listen((user) => currentUser = currentUser?.merge(user) ?? user), - _client + .listen((user) { + currentUser = currentUser?.merge(user) ?? user; + })) + ..add(_client .on() .map((event) => event.unreadChannels) .whereType() .listen((count) { currentUser = currentUser?.copyWith(unreadChannels: count); - }), - _client + })) + ..add(_client .on() .map((event) => event.totalUnreadCount) .whereType() .listen((count) { currentUser = currentUser?.copyWith(totalUnreadCount: count); - }), - ]); + })); _listenChannelDeleted(); @@ -1504,56 +1522,73 @@ class ClientState { _listenAllChannelsRead(); } - final _subscriptions = []; + /// Stops listening to the client events. + void cancelEventSubscription() { + if (_eventsSubscription != null) { + _eventsSubscription!.cancel(); + _eventsSubscription = null; + } + } - /// Used internally for optimistic update of unread count - set totalUnreadCount(int unreadCount) { - _totalUnreadCountController.add(unreadCount); + /// Pauses listening to the client events. + void pauseEventSubscription([Future? resumeSignal]) { + _eventsSubscription?.pause(resumeSignal); + } + + /// Resumes listening to the client events. + void resumeEventSubscription() { + _eventsSubscription?.resume(); } void _listenChannelHidden() { - _subscriptions - .add(_client.on(EventType.channelHidden).listen((event) async { - final eventChannel = event.channel!; - await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]); - channels[eventChannel.cid]?.dispose(); - channels = channels..remove(eventChannel.cid); - })); + _eventsSubscription?.add( + _client.on(EventType.channelHidden).listen((event) async { + final eventChannel = event.channel!; + await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]); + channels[eventChannel.cid]?.dispose(); + channels = channels..remove(eventChannel.cid); + }), + ); } void _listenUserUpdated() { - _subscriptions.add(_client.on(EventType.userUpdated).listen((event) { - if (event.user!.id == currentUser!.id) { - currentUser = OwnUser.fromJson(event.user!.toJson()); - } - updateUser(event.user); - })); + _eventsSubscription?.add( + _client.on(EventType.userUpdated).listen((event) { + if (event.user!.id == currentUser!.id) { + currentUser = OwnUser.fromJson(event.user!.toJson()); + } + updateUser(event.user); + }), + ); } void _listenAllChannelsRead() { - _subscriptions - .add(_client.on(EventType.notificationMarkRead).listen((event) { - if (event.cid == null) { - channels.forEach((key, value) { - value.state?.unreadCount = 0; - }); - } - })); + _eventsSubscription?.add( + _client.on(EventType.notificationMarkRead).listen((event) { + if (event.cid == null) { + channels.forEach((key, value) { + value.state?.unreadCount = 0; + }); + } + }), + ); } void _listenChannelDeleted() { - _subscriptions.add(_client - .on( - EventType.channelDeleted, - EventType.notificationRemovedFromChannel, - EventType.notificationChannelDeleted, - ) - .listen((Event event) async { - final eventChannel = event.channel!; - await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]); - channels[eventChannel.cid]?.dispose(); - channels = channels..remove(eventChannel.cid); - })); + _eventsSubscription?.add( + _client + .on( + EventType.channelDeleted, + EventType.notificationRemovedFromChannel, + EventType.notificationChannelDeleted, + ) + .listen((Event event) async { + final eventChannel = event.channel!; + await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]); + channels[eventChannel.cid]?.dispose(); + channels = channels..remove(eventChannel.cid); + }), + ); } final StreamChatClient _client; @@ -1615,6 +1650,11 @@ class ClientState { _channelsController.add(newChannels); } + /// Used internally for optimistic update of unread count + set totalUnreadCount(int unreadCount) { + _totalUnreadCountController.add(unreadCount); + } + void _computeUnreadCounts(OwnUser? user) { final totalUnreadCount = user?.totalUnreadCount; if (totalUnreadCount != null) { @@ -1635,7 +1675,7 @@ class ClientState { /// Call this method to dispose this object void dispose() { - _subscriptions.forEach((s) => s.cancel()); + cancelEventSubscription(); _currentUserController.close(); _unreadChannelsController.close(); _totalUnreadCountController.close(); From 31f92922cbea7e5e24d406157800bbe2c9f8cc58 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 29 Aug 2022 15:24:29 +0530 Subject: [PATCH 45/49] chore(llc): update CHANGELOG.md Signed-off-by: xsahil03x --- packages/stream_chat/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 6b73d0ab..c1809c1c 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +🐞 Fixed + +- Only listen to client events when the user is connected to the websocket. + ## 4.5.0 🐞 Fixed From 5b179febd6e70e5b9e42d5b7eea2440031dda60a Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 10 Aug 2022 18:06:39 +0530 Subject: [PATCH 46/49] chore(llc): move files to correct folders. Signed-off-by: xsahil03x --- .../stream_chat/lib/src/client/client.dart | 2 +- .../lib/src/core/api/call_api.dart | 14 ++-- .../lib/src/core/api/responses.dart | 67 +---------------- .../lib/src/core/api/responses.g.dart | 20 ------ .../lib/src/core/api/stream_chat_api.dart | 2 +- .../lib/src/core/models/call_payload.dart | 72 +++++++++++++++++++ .../lib/src/core/models/call_payload.g.dart | 27 +++++++ .../test/src/core/api/call_api_test.dart | 8 +-- .../test/src/core/api/responses_test.dart | 55 ++++---------- .../src/core/models/call_payload_test.dart | 38 ++++++++++ 10 files changed, 162 insertions(+), 143 deletions(-) create mode 100644 packages/stream_chat/lib/src/core/models/call_payload.dart create mode 100644 packages/stream_chat/lib/src/core/models/call_payload.g.dart create mode 100644 packages/stream_chat/test/src/core/models/call_payload_test.dart diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 73f0f95e..07e6ed3e 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -595,7 +595,7 @@ class StreamChatClient { required String callType, required String channelType, required String channelId, - }) async { + }) { return _chatApi.call.createCall( callId: callId, callType: callType, diff --git a/packages/stream_chat/lib/src/core/api/call_api.dart b/packages/stream_chat/lib/src/core/api/call_api.dart index 60fc6f88..2e40fc63 100644 --- a/packages/stream_chat/lib/src/core/api/call_api.dart +++ b/packages/stream_chat/lib/src/core/api/call_api.dart @@ -1,7 +1,7 @@ import 'package:stream_chat/src/core/api/responses.dart'; import 'package:stream_chat/src/core/http/stream_http_client.dart'; -/// Defines the api dedicated to video call operations +/// Defines the api dedicated to call operations. class CallApi { /// Initialize a new call api CallApi(this._client); @@ -25,11 +25,13 @@ class CallApi { required String channelType, required String channelId, }) async { - final response = - await _client.post(_getChannelUrl(channelId, channelType), data: { - 'id': callId, - 'type': callType, - }); + final response = await _client.post( + _getChannelUrl(channelId, channelType), + data: { + 'id': callId, + 'type': callType, + }, + ); // return response.data; return CreateCallPayload.fromJson(response.data); } diff --git a/packages/stream_chat/lib/src/core/api/responses.dart b/packages/stream_chat/lib/src/core/api/responses.dart index ddeb4b0f..54b885f4 100644 --- a/packages/stream_chat/lib/src/core/api/responses.dart +++ b/packages/stream_chat/lib/src/core/api/responses.dart @@ -1,9 +1,9 @@ -import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/client/client.dart'; import 'package:stream_chat/src/core/api/call_api.dart'; import 'package:stream_chat/src/core/error/error.dart'; import 'package:stream_chat/src/core/models/banned_user.dart'; +import 'package:stream_chat/src/core/models/call_payload.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; import 'package:stream_chat/src/core/models/channel_state.dart'; import 'package:stream_chat/src/core/models/device.dart'; @@ -498,51 +498,6 @@ class OGAttachmentResponse extends _BaseResponse { _$OGAttachmentResponseFromJson(json); } -/// Payload for Agora call. -@JsonSerializable(createToJson: false) -class AgoraPayload { - /// Create a new instance. - const AgoraPayload({required this.channel}); - - /// Create a new instance from a [json]. - factory AgoraPayload.fromJson(Map json) => - _$AgoraPayloadFromJson(json); - - /// The Agora channel. - final String channel; -} - -/// Model containing the information about a call. -@JsonSerializable(createToJson: false) -class CallPayload extends Equatable { - /// Create a new instance. - const CallPayload({ - required this.id, - required this.provider, - this.agora, - this.hms, - }); - - /// Create a new instance from a [json]. - factory CallPayload.fromJson(Map json) => - _$CallPayloadFromJson(json); - - /// The call id. - final String id; - - /// The call provider. - final String provider; - - /// The payload specific to Agora. - final AgoraPayload? agora; - - /// The payload specific to 100ms. - final HMSPayload? hms; - - @override - List get props => [id, provider, agora, hms]; -} - /// The response to [CallApi.getCallToken] @JsonSerializable(createToJson: false) class CallTokenPayload extends _BaseResponse { @@ -570,23 +525,3 @@ class CreateCallPayload extends _BaseResponse { /// The call object. CallPayload? call; } - -/// Payload for 100ms call. -@JsonSerializable(createToJson: false) -class HMSPayload extends Equatable { - /// Create a new instance. - const HMSPayload({required this.roomId, required this.roomName}); - - /// Create a new instance from a [json]. - factory HMSPayload.fromJson(Map json) => - _$HMSPayloadFromJson(json); - - /// The id of the 100ms room. - final String roomId; - - /// The name of the 100ms room. - final String roomName; - - @override - List get props => [roomId, roomName]; -} diff --git a/packages/stream_chat/lib/src/core/api/responses.g.dart b/packages/stream_chat/lib/src/core/api/responses.g.dart index 6cc2158e..24f5c459 100644 --- a/packages/stream_chat/lib/src/core/api/responses.g.dart +++ b/packages/stream_chat/lib/src/core/api/responses.g.dart @@ -298,21 +298,6 @@ OGAttachmentResponse _$OGAttachmentResponseFromJson( ..titleLink = json['title_link'] as String? ..type = json['type'] as String?; -AgoraPayload _$AgoraPayloadFromJson(Map json) => AgoraPayload( - channel: json['channel'] as String, - ); - -CallPayload _$CallPayloadFromJson(Map json) => CallPayload( - id: json['id'] as String, - provider: json['provider'] as String, - agora: json['agora'] == null - ? null - : AgoraPayload.fromJson(json['agora'] as Map), - hms: json['hms'] == null - ? null - : HMSPayload.fromJson(json['hms'] as Map), - ); - CallTokenPayload _$CallTokenPayloadFromJson(Map json) => CallTokenPayload() ..duration = json['duration'] as String? @@ -326,8 +311,3 @@ CreateCallPayload _$CreateCallPayloadFromJson(Map json) => ..call = json['call'] == null ? null : CallPayload.fromJson(json['call'] as Map); - -HMSPayload _$HMSPayloadFromJson(Map json) => HMSPayload( - roomId: json['room_id'] as String, - roomName: json['room_name'] as String, - ); diff --git a/packages/stream_chat/lib/src/core/api/stream_chat_api.dart b/packages/stream_chat/lib/src/core/api/stream_chat_api.dart index c19b81a9..b3f29210 100644 --- a/packages/stream_chat/lib/src/core/api/stream_chat_api.dart +++ b/packages/stream_chat/lib/src/core/api/stream_chat_api.dart @@ -56,7 +56,7 @@ class StreamChatApi { CallApi? _call; - /// Api dedicated to guest operations + /// Api dedicated to call operations CallApi get call => _call ??= CallApi(_client); ChannelApi? _channel; diff --git a/packages/stream_chat/lib/src/core/models/call_payload.dart b/packages/stream_chat/lib/src/core/models/call_payload.dart new file mode 100644 index 00000000..e11ae788 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/call_payload.dart @@ -0,0 +1,72 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'call_payload.g.dart'; + +/// Model containing the information about a call. +@JsonSerializable(createToJson: false) +class CallPayload extends Equatable { + /// Create a new instance. + const CallPayload({ + required this.id, + required this.provider, + this.agora, + this.hms, + }); + + /// Create a new instance from a [json]. + factory CallPayload.fromJson(Map json) => + _$CallPayloadFromJson(json); + + /// The call id. + final String id; + + /// The call provider. + final String provider; + + /// The payload specific to Agora. + final AgoraPayload? agora; + + /// The payload specific to 100ms. + final HMSPayload? hms; + + @override + List get props => [id, provider, agora, hms]; +} + +/// Payload for Agora call. +@JsonSerializable(createToJson: false) +class AgoraPayload extends Equatable { + /// Create a new instance. + const AgoraPayload({required this.channel}); + + /// Create a new instance from a [json]. + factory AgoraPayload.fromJson(Map json) => + _$AgoraPayloadFromJson(json); + + /// The Agora channel. + final String channel; + + @override + List get props => [channel]; +} + +/// Payload for 100ms call. +@JsonSerializable(createToJson: false) +class HMSPayload extends Equatable { + /// Create a new instance. + const HMSPayload({required this.roomId, required this.roomName}); + + /// Create a new instance from a [json]. + factory HMSPayload.fromJson(Map json) => + _$HMSPayloadFromJson(json); + + /// The id of the 100ms room. + final String roomId; + + /// The name of the 100ms room. + final String roomName; + + @override + List get props => [roomId, roomName]; +} diff --git a/packages/stream_chat/lib/src/core/models/call_payload.g.dart b/packages/stream_chat/lib/src/core/models/call_payload.g.dart new file mode 100644 index 00000000..bc786cc5 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/call_payload.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'call_payload.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CallPayload _$CallPayloadFromJson(Map json) => CallPayload( + id: json['id'] as String, + provider: json['provider'] as String, + agora: json['agora'] == null + ? null + : AgoraPayload.fromJson(json['agora'] as Map), + hms: json['hms'] == null + ? null + : HMSPayload.fromJson(json['hms'] as Map), + ); + +AgoraPayload _$AgoraPayloadFromJson(Map json) => AgoraPayload( + channel: json['channel'] as String, + ); + +HMSPayload _$HMSPayloadFromJson(Map json) => HMSPayload( + roomId: json['room_id'] as String, + roomName: json['room_name'] as String, + ); diff --git a/packages/stream_chat/test/src/core/api/call_api_test.dart b/packages/stream_chat/test/src/core/api/call_api_test.dart index 662f84b0..f9e2265f 100644 --- a/packages/stream_chat/test/src/core/api/call_api_test.dart +++ b/packages/stream_chat/test/src/core/api/call_api_test.dart @@ -23,12 +23,8 @@ void main() { const callId = 'test-call-id'; const path = '/calls/$callId'; - when(() => client.post( - path, - data: {}, - )) - .thenAnswer( - (_) async => successResponse(path, data: {})); + when(() => client.post(path, data: {})).thenAnswer( + (_) async => successResponse(path, data: {})); final res = await callApi.getCallToken(callId); diff --git a/packages/stream_chat/test/src/core/api/responses_test.dart b/packages/stream_chat/test/src/core/api/responses_test.dart index 75a374fb..073d2b40 100644 --- a/packages/stream_chat/test/src/core/api/responses_test.dart +++ b/packages/stream_chat/test/src/core/api/responses_test.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:stream_chat/src/core/models/call_payload.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; @@ -4341,45 +4342,21 @@ void main() { expect(response.message, isA()); }); - test('AgoraPayload', () { + test('CallTokenPayload', () { const jsonExample = ''' - {"channel":"test"} - '''; - final response = AgoraPayload.fromJson(json.decode(jsonExample)); - expect(response.channel, isA()); - }); - - test('CallPayload', () { - const jsonExample = ''' - {"id":"test", - "provider": "test", - "agora": {"channel":"test"}, - "hms":{"room_id":"test", "room_name":"test"} - } - '''; - final response = CallPayload.fromJson(json.decode(jsonExample)); - expect(response.agora, isA()); - expect(response.hms, isA()); - expect(response.id, isA()); - expect(response.provider, isA()); - }); - }); - - test('CallTokenPayload', () { - const jsonExample = ''' {"duration": "3ms", "agora_app_id":"test", "agora_uid": 12, "token": "token"} '''; - final response = CallTokenPayload.fromJson(json.decode(jsonExample)); - expect(response.agoraAppId, isA()); - expect(response.agoraUid, isA()); - expect(response.token, isA()); - }); + final response = CallTokenPayload.fromJson(json.decode(jsonExample)); + expect(response.agoraAppId, isA()); + expect(response.agoraUid, isA()); + expect(response.token, isA()); + }); - test('CreateCallPayload', () { - const jsonExample = ''' + test('CreateCallPayload', () { + const jsonExample = ''' {"call": {"id":"test", "provider": "test", @@ -4387,16 +4364,8 @@ void main() { "hms":{"room_id":"test", "room_name":"test"} }} '''; - final response = CreateCallPayload.fromJson(json.decode(jsonExample)); - expect(response.call, isA()); - }); - - test('HMSPayload', () { - const jsonExample = ''' - {"room_id":"test", "room_name":"test"} - '''; - final response = HMSPayload.fromJson(json.decode(jsonExample)); - expect(response.roomId, isA()); - expect(response.roomName, isA()); + final response = CreateCallPayload.fromJson(json.decode(jsonExample)); + expect(response.call, isA()); + }); }); } diff --git a/packages/stream_chat/test/src/core/models/call_payload_test.dart b/packages/stream_chat/test/src/core/models/call_payload_test.dart new file mode 100644 index 00000000..a67e3bee --- /dev/null +++ b/packages/stream_chat/test/src/core/models/call_payload_test.dart @@ -0,0 +1,38 @@ +import 'dart:convert'; + +import 'package:stream_chat/src/core/models/call_payload.dart'; +import 'package:test/test.dart'; + +void main() { + test('CallPayload', () { + const jsonExample = ''' + {"id":"test", + "provider": "test", + "agora": {"channel":"test"}, + "hms":{"room_id":"test", "room_name":"test"} + } + '''; + final response = CallPayload.fromJson(json.decode(jsonExample)); + expect(response.agora, isA()); + expect(response.hms, isA()); + expect(response.id, isA()); + expect(response.provider, isA()); + }); + + test('AgoraPayload', () { + const jsonExample = ''' + {"channel":"test"} + '''; + final response = AgoraPayload.fromJson(json.decode(jsonExample)); + expect(response.channel, isA()); + }); + + test('HMSPayload', () { + const jsonExample = ''' + {"room_id":"test", "room_name":"test"} + '''; + final response = HMSPayload.fromJson(json.decode(jsonExample)); + expect(response.roomId, isA()); + expect(response.roomName, isA()); + }); +} From 7faa765d14dfa3522acf8f957b36f3d8a24d2f37 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 13 Sep 2022 10:24:25 +0200 Subject: [PATCH 47/49] fix(ui): Fix message text hiding because of a flutter bug (#1327) * fix(ui): remove clip * chore(ui): update changelog --- packages/stream_chat_flutter/CHANGELOG.md | 6 ++++++ .../example/ios/Flutter/AppFrameworkInfo.plist | 2 +- .../example/ios/Runner.xcodeproj/project.pbxproj | 6 +++--- packages/stream_chat_flutter/lib/src/message_widget.dart | 1 - 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 54fc55bd..83d21419 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +🐞 Fixed + +- [[#1323]](https://github.com/GetStream/stream-chat-flutter/issues/1323): Fix message text hiding because of a [flutter bug](https://github.com/flutter/flutter/issues/110628). + ## 4.5.0 - Updated `stream_chat_flutter_core` dependency diff --git a/packages/stream_chat_flutter/example/ios/Flutter/AppFrameworkInfo.plist b/packages/stream_chat_flutter/example/ios/Flutter/AppFrameworkInfo.plist index f2872cf4..4f8d4d24 100644 --- a/packages/stream_chat_flutter/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/stream_chat_flutter/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj index 2550e7a5..9b47e613 100644 --- a/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -339,7 +339,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -425,7 +425,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -474,7 +474,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 71d0ab2c..8164f08c 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -764,7 +764,6 @@ class _StreamMessageWidgetState extends State ), ) : Card( - clipBehavior: Clip.hardEdge, elevation: 0, margin: EdgeInsets.symmetric( From 3e1d6e91c705c28fd75d84fcd45ff4f186a05ee9 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 15 Sep 2022 08:30:20 +0200 Subject: [PATCH 48/49] chore(llc,core,ui): update pubspec and changelogs (#1333) --- packages/stream_chat/CHANGELOG.md | 2 +- packages/stream_chat/lib/version.dart | 2 +- packages/stream_chat/pubspec.yaml | 2 +- packages/stream_chat_flutter/CHANGELOG.md | 2 +- packages/stream_chat_flutter/pubspec.yaml | 4 ++-- packages/stream_chat_flutter_core/CHANGELOG.md | 4 ++++ packages/stream_chat_flutter_core/example/pubspec.yaml | 2 +- packages/stream_chat_flutter_core/pubspec.yaml | 5 ++--- packages/stream_chat_localizations/pubspec.yaml | 2 +- packages/stream_chat_persistence/example/pubspec.yaml | 2 +- packages/stream_chat_persistence/pubspec.yaml | 2 +- 11 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 48446817..86ce4e63 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,4 +1,4 @@ -## Upcoming +## 4.6.0 ✅ Added diff --git a/packages/stream_chat/lib/version.dart b/packages/stream_chat/lib/version.dart index 5d9e926b..3acdad25 100644 --- a/packages/stream_chat/lib/version.dart +++ b/packages/stream_chat/lib/version.dart @@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart'; /// Current package version /// Used in [StreamChatClient] to build the `x-stream-client` header // ignore: constant_identifier_names -const PACKAGE_VERSION = '4.5.0'; +const PACKAGE_VERSION = '4.6.0'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index c3d14d9c..63c8b06c 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat homepage: https://getstream.io/ description: The official Dart client for Stream Chat, a service for building chat applications. -version: 4.5.0 +version: 4.6.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 83d21419..d7620978 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,4 +1,4 @@ -## Upcoming +## 4.6.0 🐞 Fixed diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 3d6a37c7..c0872593 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter. -version: 4.5.0 +version: 4.6.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -36,7 +36,7 @@ dependencies: rxdart: ^0.27.0 share_plus: ^4.0.1 shimmer: ^2.0.0 - stream_chat_flutter_core: ^4.5.0 + stream_chat_flutter_core: ^4.6.0 substring_highlight: ^1.0.26 url_launcher: ^6.1.0 video_player: ^2.1.0 diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index d4288b9a..eb855ec1 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.6.0 + +- Updated `stream_chat` dependency to [`4.6.0`](https://pub.dev/packages/stream_chat/changelog). + ## 4.5.0 - Updated `stream_chat` dependency to [`4.5.0`](https://pub.dev/packages/stream_chat/changelog). diff --git a/packages/stream_chat_flutter_core/example/pubspec.yaml b/packages/stream_chat_flutter_core/example/pubspec.yaml index 59afbf41..4ce0c7e9 100644 --- a/packages/stream_chat_flutter_core/example/pubspec.yaml +++ b/packages/stream_chat_flutter_core/example/pubspec.yaml @@ -26,7 +26,7 @@ dependencies: cupertino_icons: ^1.0.3 flutter: sdk: flutter - stream_chat_flutter_core: ^4.4.1 + stream_chat_flutter_core: ^4.6.0 dev_dependencies: flutter_test: sdk: flutter diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index 989245c6..a8f1489e 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter_core homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter. -version: 4.5.0 +version: 4.6.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -17,8 +17,7 @@ dependencies: freezed_annotation: ^2.0.3 meta: ^1.3.0 rxdart: ^0.27.0 - stream_chat: ^4.5.0 - + stream_chat: ^4.6.0 dev_dependencies: build_runner: ^2.0.1 dart_code_metrics: ^4.4.0 diff --git a/packages/stream_chat_localizations/pubspec.yaml b/packages/stream_chat_localizations/pubspec.yaml index 2c819f55..88f4ab6a 100644 --- a/packages/stream_chat_localizations/pubspec.yaml +++ b/packages/stream_chat_localizations/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - stream_chat_flutter: ^4.4.1 + stream_chat_flutter: ^4.6.0 dev_dependencies: dart_code_metrics: ^4.4.0 flutter_test: diff --git a/packages/stream_chat_persistence/example/pubspec.yaml b/packages/stream_chat_persistence/example/pubspec.yaml index 3c47bd1b..5043fa9f 100644 --- a/packages/stream_chat_persistence/example/pubspec.yaml +++ b/packages/stream_chat_persistence/example/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: cupertino_icons: ^1.0.3 flutter: sdk: flutter - stream_chat: ^4.4.1 + stream_chat: ^4.6.0 stream_chat_persistence: ^4.4.0 dev_dependencies: flutter_test: diff --git a/packages/stream_chat_persistence/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index b2ac03f2..a2c9feb4 100644 --- a/packages/stream_chat_persistence/pubspec.yaml +++ b/packages/stream_chat_persistence/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: path: ^1.8.0 path_provider: ^2.0.1 sqlite3_flutter_libs: ^0.5.0 - stream_chat: ^4.4.1 + stream_chat: ^4.6.0 dev_dependencies: build_runner: ^2.0.1 dart_code_metrics: ^4.4.0 From 4f2542291c1090bbfa986b8816db53792dc14029 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 30 Sep 2022 17:55:57 +0200 Subject: [PATCH 49/49] V5 (#1222) * chore(flutter): change BoxFit of VLC player * chore(flutter): remove unused imports * chore(flutter sample): tweak breakpoints, ignore some lints * chore(flutter): minor code readability improvements * chore(flutter): extract out platform_widget_builder to its own "package" * fix(flutter): fix a theme bug with PlatformDialog * chore(persistence): test plugin on macOS * Generated the macos runner for the example * Re-generated the android runner because it was using the v1 embedding * chore(persistence): test plugin on web * chore(persistence): add Windows runner to example * chore(persistence): add linux runner to example * feat(flutter): branded context menus * chore(flutter): fix bug where lines would be shown in between menu items * chore(flutter): import/export fixes * chore(flutter): move file * chore(flutter): /src/channel Also refactors several /channel widgets to use class widgets instead of methods. Some methods, however, have been left as those cases do actually produce cleaner code. chore(flutter): fix bad directory placement chore(flutter): fix bad directory placement * chore(flutter): more directory organization * chore(flutter): navigator improvements * chore(flutter): void async improvements * chore(flutter): fix imports/exports * docs(flutter): documentation updates for /attachment * docs(flutter): doc updates For: * /avatars * /bottom_sheets * /channel * feat(flutter): selectable message text on web & desktop * Squashed commit of the following: commit 50b249edb6b2ee2d744e4e00e97f81979026aab4 Author: GroovinChip Date: Wed Mar 16 14:55:05 2022 -0400 chore(flutter): wrapping up docs commit 7d289ae45e20c0109bcdb4496f7b693e2adc4ef9 Author: GroovinChip Date: Wed Mar 16 14:42:09 2022 -0400 chore(flutter): more docs & typedefs commit 6b36f6f3864f15b52e441d58ee7e62577ff8f843 Author: GroovinChip Date: Wed Mar 16 14:22:34 2022 -0400 chore(flutter): more doc fixes commit 1a9a6de3c836e9ee5d6373033f1d9a7da830bddc Merge: 59188f5d 88abf99f Author: GroovinChip Date: Wed Mar 16 13:17:52 2022 -0400 Merge branch 'desk_web_dev' into utils commit 59188f5d9c1dd68edd531086cdfd9d42faf93fc8 Author: GroovinChip Date: Wed Mar 16 12:48:16 2022 -0400 chore(flutter): add todo commit 9b4a986ca5757f284677eb5335637d11ccb1d642 Author: GroovinChip Date: Wed Mar 16 12:43:12 2022 -0400 chore(flutter): more docs and typedefs updates commit 8786d2dd82b94f673b36371fb2b33a1ebcc9840a Author: GroovinChip Date: Wed Mar 16 12:14:14 2022 -0400 chore(flutter): more typedef and doc template updates commit 9e906beb0e7a054c13a32c8c8c5b0e2a9e27bb21 Author: GroovinChip Date: Wed Mar 16 10:46:04 2022 -0400 chore(flutter): create /uitils and move files Fixes subsequent imports/exports * chore(flutter): update file_selector plugin * chore(flutter): include utils/typedefs.dart in library export * Squashed commit of the following: commit 501228afa614b0d740858dec1ebedd3f671e2c96 Author: GroovinChip Date: Thu Mar 17 16:18:12 2022 -0400 feat(flutter): ensure fullscreen media feature works on ALL platforms commit cd0aab92bc7d90af03258829a5aae66125578af5 Author: GroovinChip Date: Thu Mar 17 14:26:48 2022 -0400 feat(flutter): working conditional import for initializing VLC * fix(flutter): remove stray import causing web to break * chore(flutter): ensure context menus can show on web in sample * feat(flutter) FullScreenMedia context menus for Web * feat(flutter): branded context menus * chore(flutter): no success dialog on file download (web) * chore(flutter): remove platform UI packages * chore(flutter): properly show/hide delete message menu item Also deletes deprecated PlatformDialog * feat(flutter): 'view info' channel preview context menu * feat(flutter): more menu actions for ChannelPreview Also new ConfirmationDialog and add missing docs * feat(flutter/localizations): translations related to channel mutes * chore(flutter): remove prints and fix super.didChangeDependencies order * chore(flutter): rename some things in ChannelBottomSheet * chore(flutter): more sensible name in TypingIndicator * chore(flutter): remove unused code in MessageWidget * feat(flutter): ChannelInfoDialog * chore(flutter): organize files more Also refactors out some internal MessageListView widgets * fix(flutter): fix render overflow issue * chore(flutter): move MediaListView * chore(flutter): remove unused imports * fix(flutter): fix weird share menu placement on macos * chore(flutter): clean up fullscreenmedia conditional imports * chore(flutter): adjust package exports * chore(flutter): update dart_vlc * chore(flutter): clean up platform checks * chore(flutter): run flutter format on lib * chore(flutter): minor sample cleanup * chore(flutter): ensure vlc doesn't get initialized in non-desktop testing environments * test(flutter): reformat existing tests * fix(flutter): pass onThreadReplyTap down to ThreadReplyButton * First round of new tests commit a90bed427521123fdaa4c5e276744a224fac2b68 Author: GroovinChip Date: Thu Apr 7 12:34:19 2022 -0400 test(flutter): Username test commit d7bc179c20eae2713c9248191518bf1d955104a9 Author: GroovinChip Date: Thu Apr 7 12:30:47 2022 -0400 chore(flutter): remove unused imports commit 09c9517de2917dc5ccd7de019b50a7875386991c Author: GroovinChip Date: Thu Apr 7 12:29:58 2022 -0400 test(flutter): ThreadSeparator test commit 9422d1129211aaaac9a51ca4d29fe94bde06593c Author: GroovinChip Date: Thu Apr 7 12:19:43 2022 -0400 chore(flutter): rename LoadingIndicator parameter commit 63af943a1a16e78a1e0377d76176420bd7e1fbc7 Author: GroovinChip Date: Thu Apr 7 11:47:46 2022 -0400 test(flutter): FloatingDateDivider test commit d8e67cf6fca2b76ac091a9047104c0910d5fdfef Author: GroovinChip Date: Tue Mar 29 15:19:39 2022 -0400 test(flutter): InputAttachment tests commit d2b7d5dd3304c8be6f74bf5b0281d7a95dd027ec Author: GroovinChip Date: Tue Mar 29 14:41:40 2022 -0400 test(flutter): DmCheckbox tests commit 01567f235a2f00880bb530b232489e3790159b25 Author: GroovinChip Date: Tue Mar 29 14:16:08 2022 -0400 chore(flutter): remove unused imports commit c9625858e840df6082506ae9374f1d2710053cbd Author: GroovinChip Date: Tue Mar 29 14:15:44 2022 -0400 test(flutter): CountdownButton tests commit d770474bb11ee8b36a3b0e18db31695fd812a6b3 Author: GroovinChip Date: Tue Mar 29 14:11:37 2022 -0400 test(flutter): CommandButton tests commit 95e63706d1dd971373d39d471894a38bb2a634f2 Author: GroovinChip Date: Tue Mar 29 14:07:43 2022 -0400 test(flutter): ClearInputItem tests commit 4156586945c91408cb7943b6c57c2ae560a2857c Author: GroovinChip Date: Tue Mar 29 14:03:59 2022 -0400 test(flutter): AttachmentButton tests commit bba042806326999aabf338636c6e33900de86160 Author: GroovinChip Date: Tue Mar 29 13:00:42 2022 -0400 test(flutter): golden test for SendButton commit e2cd0f00b9eeb8d81399f6316edb298234de12dc Author: GroovinChip Date: Tue Mar 29 12:56:52 2022 -0400 chore(flutter): deal with lints commit b4e045f9cbd54c4d42c3f30acfe72d5f8ccf9d82 Author: GroovinChip Date: Tue Mar 29 12:56:00 2022 -0400 test(flutter): SendButton test commit 4e3d5db0bddb97e508fcdda2e7272fe31e70808f Author: GroovinChip Date: Tue Mar 29 12:38:21 2022 -0400 test(flutter): KeyboardShortcutRunner tests commit 0dfd7a0bd6fd6bc76902e87f62ac29da0b01bac6 Author: GroovinChip Date: Tue Mar 29 12:17:30 2022 -0400 test(flutter): UploadProgressIndicator commit ec50c8aea716bdc19e4308e367c8331211c61ea3 Author: GroovinChip Date: Tue Mar 29 11:47:53 2022 -0400 test(flutter): SendingIndicator tests commit e87bd1450fb5bfb713a867c7e831968f76428848 Author: GroovinChip Date: Tue Mar 29 11:32:35 2022 -0400 test(flutter): more gallery tests commit 93d46ca649277fe6d289a70f0bdd23584fd0ab94 Author: GroovinChip Date: Tue Mar 29 11:17:29 2022 -0400 test(flutter): remove bad mock commit c7b03a39f0f3ca7a334dfb908f3473b5e4deab38 Author: GroovinChip Date: Tue Mar 29 10:45:45 2022 -0400 chore(flutter): minor refactor commit 33aeb7b5ed398287cb930f3b4702f47a82fce2c6 Author: GroovinChip Date: Tue Mar 29 09:49:07 2022 -0400 test(flutter): MessageDialog tests commit 220b3ae591e135d9606254666e04d4771945d0ae Author: GroovinChip Date: Tue Mar 29 09:42:51 2022 -0400 test(flutter): DeleteMessageDialog tests commit db508478bb96749971f563de87b6c2a49e1e4e57 Author: GroovinChip Date: Mon Mar 28 14:46:41 2022 -0400 test(flutter): ConfirmationDialog tests commit e8172dad6bfe18f40b65554ef2c6c16d7437cb56 Author: GroovinChip Date: Mon Mar 28 14:35:50 2022 -0400 test(flutter): ChannelInfoDialog test commit 04c21458a601af3c3f4a5fc5ce9e8b61493c208c Author: GroovinChip Date: Mon Mar 28 13:57:02 2022 -0400 test(flutter): DownloadMenuItem tests commit afee1dace543e2fef8543f9b035f684b2ee00f3b Author: GroovinChip Date: Mon Mar 28 13:53:21 2022 -0400 test(flutter): StreamChatContextMenuItem tests commit 4616b1a973d0cf5224b5a71914c9908339c8462a Author: GroovinChip Date: Mon Mar 28 13:02:07 2022 -0400 test(flutter): golden test for ErrorAlertSheet commit 22fb8241ae333f57ade8e8550f87695929836b2b Author: GroovinChip Date: Mon Mar 28 12:59:01 2022 -0400 test(flutter): golden test for EditMessageSheet commit 0be79964827f344ba66e99cb9e5bd3d627aed01d Author: GroovinChip Date: Mon Mar 28 12:56:08 2022 -0400 test(flutter): golden test for AttachmentModalSheet commit 0ae842a98143478885bc211c788353c8bca57b40 Author: GroovinChip Date: Mon Mar 28 12:53:26 2022 -0400 test(flutter): GroupAvatar tests commit 697d294c6c7e1bba1d9089a53d5c60d8609ea64e Author: GroovinChip Date: Mon Mar 28 12:30:25 2022 -0400 test(flutter): UserAvatar tests commit c03ba4124068c83d3a49c4db00ca18a659801c23 Author: GroovinChip Date: Mon Mar 28 11:04:55 2022 -0400 test(flutter): ErrorAlertSheet test commit 3414f28530b0e22614b83af20361f21dd4967fae Author: GroovinChip Date: Mon Mar 28 10:45:37 2022 -0400 test(flutter): EditMessageSheet test commit b2962b641dfbf14c11d3da7f80a4be27f9980d90 Author: GroovinChip Date: Mon Mar 28 10:37:18 2022 -0400 test(flutter): AttachmentModalSheet test test(flutter): AttachmentModalSheet test chore(flutter): update exports commit 7d7e278692c65dea84e7ef3b2d3c1311ebb28408 Author: GroovinChip Date: Mon Mar 28 09:52:29 2022 -0400 test(flutter): more organization commit 4479b649835acc62d8a0553f9505a0af8077abfc Author: GroovinChip Date: Mon Mar 28 09:33:06 2022 -0400 test(flutter): tweak AttachmentHandler tests commit b4a41106567e3c08105ce8dc272b1ebf40b3b0bb Author: GroovinChip Date: Mon Mar 28 09:24:00 2022 -0400 chore(flutter): streamline attachment handling * MobileAttachmentHandler for streamlining related code for mobile * Break out DesktopAttachmentHandler into its own file * Update minSdk version in Android runner commit bfb06d31b79cd941d8b611c7e89e2d7728a394cb Merge: a44b3b39 15c83d03 Author: GroovinChip Date: Fri Mar 25 13:00:27 2022 -0400 Merge branch 'desk_web_dev' into tests commit a44b3b390adc25a00d1597bb1460973e8555fa9e Author: GroovinChip Date: Fri Mar 25 12:59:14 2022 -0400 chore(flutter): start cleaning up test dir commit de104a3491d942d12f72dd66152b62c6e8b10a5b Author: GroovinChip Date: Fri Mar 25 12:56:48 2022 -0400 test(flutter): VideoAttachment test commit 4748a1c081a3272f7fdbdeb69c501344e221e01e Author: GroovinChip Date: Fri Mar 25 12:52:30 2022 -0400 test(flutter): UrlAttachment test commit 54408ef70f07c60adf8350553fdf3173e5edd7c7 Author: GroovinChip Date: Fri Mar 25 12:48:24 2022 -0400 test(flutter): ImageGroup test commit 811cff069e490c6bd03be0c0ac2f5ec86fb990e9 Author: GroovinChip Date: Fri Mar 25 12:46:04 2022 -0400 test(flutter): ImageAttachment test commit a72677976c6524e76793432b0520e99cac4e7a62 Author: GroovinChip Date: Fri Mar 25 12:34:06 2022 -0400 test(flutter): GiphyAttachment test commit 32ddac022e2c149726b4b6607f41850d556055b8 Author: GroovinChip Date: Fri Mar 25 12:28:01 2022 -0400 test(flutter): FileAttachment test commit 6534958cab775be1fb9bda56e67caa4746aa1502 Author: GroovinChip Date: Fri Mar 25 12:14:25 2022 -0400 test(flutter): AttachmentError test commit 718a83c10c4e883ce01c9c002ced24de671d30b9 Author: GroovinChip Date: Fri Mar 25 12:06:43 2022 -0400 test(flutter): AttachmentUploadStateBuilder test commit a05108c460e36b2e425e4ec06f8bf408b0c20e97 Author: GroovinChip Date: Fri Mar 25 11:44:34 2022 -0400 chore(flutter): minor cleanup commit 6e73038ac3714a2bd96f8c587af7f743bf2b2023 Author: GroovinChip Date: Fri Mar 25 11:43:23 2022 -0400 test(flutter): AttachmentTitle test commit c6ee52ec951877feef31dc1427eec3bfc2bd8fcb Author: GroovinChip Date: Fri Mar 25 11:43:11 2022 -0400 test(flutter): minor cleanup commit 09845cd845aa8c546043f4ae04805a8e6f53f61c Author: GroovinChip Date: Fri Mar 25 11:42:52 2022 -0400 chore(flutter): improve device segmentation & vlc init check commit 172e6b25a064b5147b7641f29ebff3c920156f70 Author: GroovinChip Date: Fri Mar 25 10:37:43 2022 -0400 test(flutter): attachment_handler tests commit 5f5a91c70cd7c069f39518f1f50bb74ab484c7e9 Author: GroovinChip Date: Wed Mar 23 13:55:27 2022 -0400 test(flutter): test PlatformWidgetBuilder & DesktopWidgetBuilder commit 5a457b7557cb4127b74ee022389a24e5e9150601 Author: GroovinChip Date: Wed Mar 23 13:55:05 2022 -0400 chore(flutter): make base platform widgets testable Also fix environment check in stream_chat.dart commit 88fb16018103c581236aeebd978e705b223b2243 Author: GroovinChip Date: Wed Mar 23 11:59:19 2022 -0400 chore(flutter): add readme to conditional_parent_widget library commit e9307f8a6a5639d59358c245f033f9d9b209fe20 Author: GroovinChip Date: Wed Mar 23 11:54:54 2022 -0400 test(flutter): ConditionalParentBuilder tests * chore(flutter): add thumblr dependency * chore(flutter): fix typos * chore(flutter): properly order super calls * feat(flutter) video thumbnails on desktop * chore(flutter): fix incorrect file name * chore(flutter): fix docs for generateVideoThumbnail * chore(flutter): remove incorrect comment * chore(flutter): run flutter format * chore(flutter): ignore lint * chore(flutter): flutter format * chore(flutter): fix tests * fix(flutter): fix MessageListView bug caused by rebase * chore(flutter): migrate desktop stuff to v4/stream_message_input.dart * chore(flutter): migrate over desktop changes for message replies * chore(flutter): fix message replies on desktop * chore(flutter): address lints * chore(flutter): update dart_vlc * chore(flutter): melos clean & update photo_view * fix(ui): port #1118 * fix(ui): use attachmentButtonBuilder * fix(ui): fix file attachment * format * fix(ui): fix message bottom row * chore(flutter sample): use modern window look * chore(flutter): pass userAvatarBuilder through to MessageWidgetContent Also pass onUserAvatarTap & userAvatarBuilder through to UserAvatarTransform * chore: fix MessageWidget menu actions * fix(flutter): fix "enter" sending empty messages in StreamMessageInput * fix(flutter): fix Giphys not loading in quoted messages * chore(flutter): update thumblr * feat(flutter): keyboard navigation for gallery items * fix(ui): fix double date divider * fix(ui): add scroll to bottom back * fix(ui): use mouse region * fix(ui): remove backbutton in desktop example * fix(ui): rename full screen media widget * feat(flutter): elevation prop for gallery header * chore(flutter): update dart_vlc * fix(flutter): fix "show in chat" showing when it shouldn't * fix(flutter): fix "show in chat" for mobile * docs(flutter): minor doc update * chore(flutter): add onMessageSwiped in example app * chore(flutter): pass usernameBuilder through to BottomRow * fix(flutter): hacky fix for chonky link previews * fix(flutter): fix chonky text messages * feat(flutter): press "esc" to close gallery Also refactors KeyboardShortcutRunner API * chore(flutter): fix innaccurate name * fix(ui): fix attachment + long text sizing * add key to channelpage * fix(ui): better example * fix format * fix example * chore(flutter): revert modern window look * improvement(flutter): improve `GalleryNavigationItem` * Make buttons more visible * Spacing from the sides of the screen * fix video thumbnail * fix split view * fix(ui): fix giphy in thread * feat(flutter): desktop message reactions commit 4b880ecb6d4dcbf315a7d41d863174817a9c530a Author: Reuben Turner Date: Thu Jun 9 12:25:21 2022 -0400 fix(flutter): fixes for desktop reactions * Fix reactions for reverse messages * Ensure reactions near the edges of MessageListView show properly commit f511513f8dbb272397e81d08bad33ee60108d738 Author: Salvatore Giordano Date: Thu Jun 9 16:43:03 2022 +0200 fix(ui): fix DesktopReactionBuilder and Portal commit c4885648330552f5212c5ba6b771c0f07023f6ae Merge: 2b710a2b 72b34c21 Author: Reuben Turner Date: Wed Jun 8 14:12:44 2022 -0400 Merge branch 'desk_web_dev' into desktop_reactions commit 2b710a2bb610326123d5b7f75c26caa705114c9b Author: Reuben Turner Date: Mon Jun 6 12:19:12 2022 -0400 chore(flutter): add debug props commit e24dabc1de12da8dce697b6d4e1a3617e9835549 Author: Reuben Turner Date: Mon Jun 6 12:00:03 2022 -0400 feat(flutter): first pass at desktop reactions popup on mouse hover * fix(flutter): potential fix for #1199 * fix(flutter): fix non-reversed reactions placement * refactor(flutter): extract bottom reaction, stacked reaction with debug props * fix(flutter): fixes for desktop reactions * Fix bottom row being rendered above reaction buttons * Fix padding issues related to avatars * chore(flutter): add back lost changes from 2b1fb6a Also removed commented code * chore(flutter): stacked reactions update * Show "You" if userId matches the reaction username (per design) * Ensure long names don't throw off alignment * chore(flutter): format * fix(ui): fix mime type generation * chore(flutter): tweak desktop video * chore(flutter): add some missing things from develop * merge develop * fix merge * fix merge * format * fix tests * increate test action timeout * remove test * refactor(ui,core): remove deprecated code and cleanup * run builder * update coverage threasholds * refactor(ui): remove arrow function from countdown button * Update packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart * Update packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart * update comment * fix(ui): fix attachment + link solution * fix(ui): fix attachment + link solution * chore(flutter) update changelog and version * chore(localizations): update changelog and versions * chore(llc,core,ui,persistence,localizations): update changelog and pubspec * chore(llc,core,ui,persistence,localizations): update changelog and pubspec * fix example * fix deps * fix(ui): fix enter sending the message while in slowmode * fix(ui): fix reaction list * fix web * fix(ui): fix image download when using assetUrl * fix(ui): fix typo in changelog * fix(ui): fix gallery navigation * fix(ui): fix giphy sending size * fix(ui): add keyboard shorcuts to gallery view * fix(ui): fix gallery footer * fix(core): allow scroll notification bubbling in lazy load scroll view * fix(ui): fix lazy load scroll view * fix(ui): fix loading to unread position * fix(ui): fix last message preview * chore(ui): update changelog * fix tests * fix(llc): fix connecting while connecting and disconneting (#1237) * fix(llc): fix connecting while connecting and disconneting * chore(llc): update changelog * chore(llc): add test * chore(llc): improve test * fix(llc): _resetRequestFlags regardless of the connection status * feat(llc): add support for AttachmentFileUploaderProvider. (#1246) * feat(llc): add support for `AttachmentFileUploaderProvider`. Signed-off-by: xsahil03x * chore(llc): update CHANGELOG.md Signed-off-by: xsahil03x * refactor(llc): Remove deprecated attachmentFileUploader field. Signed-off-by: xsahil03x * port no localization * fix(ui): fix jiffy locale initialization (#1255) * fix(ui): fix jiffy locale initialization * chore(ui): update changelog * fix(ui): cleanup * chore(llc,core,ui,persistence,localizations): align changelog * chore(llc,core,ui,persistence,localizations): add links to 4.x changelogs * fix(ui): fix date divider not showing (#1259) * fix(ui): fix date divider not showing * chore(ui): update changelog * chore(ui): minor changes Signed-off-by: xsahil03x * fix(ui): fix separator Signed-off-by: xsahil03x Co-authored-by: Sahil Kumar * chore(ui): add contextmenu requirements for web (#1261) * fix unread separator * feat(ui): `StreamChatConfiguration` class (#1128) * feat: StreamChatConfiguration class See #1125 for more details * chore(flutter): update changelog * fix analysis * cleanup * feat(ui): add enforceUniqueReactions to `StreamChatConfiguration` widget * add tests * refactor(ui): rename StreamChatConfigurationProvider and StreamChatConfiguration * refactor(ui): rename StreamChat.config to streamChatConfigData * refactor(ui): make enforceUniqueReactions non nullable * chore(ui): fix analysis errors * refactor(ui): make config fields non nullable * chore(ui): format Co-authored-by: Salvatore Giordano * feat(ui, core): add StreamAutocomplete (#1263) * feat(ui, core): add stream_autocomplete.dart Signed-off-by: xsahil03x * refactor(core, ui): refactor message input controller Signed-off-by: xsahil03x * fix(ui): minor fixes Signed-off-by: xsahil03x * feat(ui): add StreamAutocompleteOptions widget Signed-off-by: xsahil03x * Update packages/stream_chat_flutter/lib/src/autocomplete/stream_autocomplete.dart * fix(ui): cleanup * chore(core, ui): cleanup Signed-off-by: xsahil03x * chore(ui): ignore no empty block * chore(ui): more cleanup Signed-off-by: xsahil03x * fix(ui): fix command button for bigger screens, other fixes. Signed-off-by: xsahil03x Co-authored-by: Salvatore Giordano * fix(ui): increase default file limit to 100MB (#1279) * fix(ui): increase default file limit to 100MB * Update packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart * fix(ui): export kDefaultMaxAttachmentSize and use it in `StreamAttachmentPicker` default * feat(ui): show dialog after clicking on the camera button and permission is denied (#1262) * feat(ui): show dialog after clicking on the camera button and permission is denied * chore(ui): update changelog * Remove permission_handler plugin and refactor * cleanup * cleanup * add tests * fix(ui): reset the permission state on resume * cleanup * fix concurrent calls * fix(ui): cleanup * fix(ui): add try catch * fix(llc, ui): copy file to tempdir before uploading a file and do not serialize bytes (#1285) * fix(llc, ui): copy file to tempdir before uploading a file and do not serialize bytes * fix(llc): do not save failed messages with attachments on web * fix(llc): replace every with any * chore(llc): add test * fix analysis * fix analysis * chore(llc,core,ui,persistence): align changelog with v4 * fix(ui): do not linkify system message mentions * chore(llc,core,ui,persistence,localizations): update changelog and pubspec (#1289) * chore(ui): update deps * chore(llc,core,ui,persistence,localizations): update changelog and pubspec * chore(ui): publish beta.2 * feat(core,ui): add StreamMemberListView, StreamMemberGridView, and StreamMemberListController (#1291) * feat(core,ui): add StreamMemberListView, StreamMemberGridView, and StreamMemberListController * chore(core,ui): update changelog * fix(ui): fix exports * chore(ui): add tests * chore(ui): add StreamMemberGridTile typedef * chore(ui): minor changes Signed-off-by: xsahil03x Co-authored-by: Sahil Kumar * fix(ui): do not send empty message * refactor(ui,localization): removed emoji picker and connected (#1292) * refactor(ui,localization): removed emoji picker and connected * chore(ui,localizations): update changelog * chore(repo): update ui coverage threashold * doc(ui): add autocomplete_triggers guide * chore(ui): add tests for isOnlyEmoji * chore(ui): add tests for isOnlyEmoji * feat(ui): add `StreamAutocompleteTrigger.triggerOnlyAfterSpace` * fix(llc): Fix `Channel.removeMessage` not able to remove thread message. (#1294) Signed-off-by: xsahil03x * chore(ui): flutter format Signed-off-by: xsahil03x * fix(ui): fix `stopTyping` unhandled exceptions when network is off or spotty. (#1296) * feat(llc, ui): Introduce `keyStrokeHandler` to properly handle keyStrokes. Signed-off-by: xsahil03x * chore(ui): update CHANGELOG.md Signed-off-by: xsahil03x * test(llc): add key_stroke_handler_test.dart Signed-off-by: xsahil03x * chore(ui): update CHANGELOG.md Signed-off-by: xsahil03x * fix(core): only handle the channel list event if the state is success. Signed-off-by: xsahil03x * feat(llc): add `hide_history` flag in `channel.addMembers`. Signed-off-by: xsahil03x * refactor(llc): Convert positional argument to optional arguments Signed-off-by: xsahil03x * test(llc): update tests Signed-off-by: xsahil03x * perf: improve image resizing * fix(core): use copyWith while setting controller items. Signed-off-by: xsahil03x * feat(ui): Added `thumbnailSize`, `thumbnailResizeType`, and `thumbnailCropType` params to `StreamMessageWidget` and `StreamAttachmentPicker` to customize the appearance of image thumbnails. Signed-off-by: xsahil03x * fix analysis * Release v4.5.0 (#1308) * chore(llc, ui, core): bump version to v4.5.0 Signed-off-by: xsahil03x * chore(ui, core): update CHANGELOG.md Signed-off-by: xsahil03x Signed-off-by: xsahil03x * third party video integration * fix llc * refactor: clean up and add docs * chore(llc): update changelog * chore(llc): add tests * chore(llc): add more tests * fix analysis * chore(llc): add more tests * chore(llc): add more tests * fix(llc): only start listening to event once connected. Signed-off-by: xsahil03x * chore(llc): update CHANGELOG.md Signed-off-by: xsahil03x * chore(llc): move files to correct folders. Signed-off-by: xsahil03x * fix(ui): Fix message text hiding because of a flutter bug (#1327) * fix(ui): remove clip * chore(ui): update changelog * cleanup * fix(ui): hitting "enter" on the android keyboard sends the message instead of going to a new line. Signed-off-by: xsahil03x * feat(ui): add support for additional text field params in message input. Signed-off-by: xsahil03x * fix analysis * remove print * chore(llc,core,ui): update pubspec and changelogs (#1333) * fix(ui): show placeholder for web video thumbs * fix file download on web * fix reply and show buttons * remove tests * treat svg as files * fix(ui): change default image resize to clip * solve some todos * polish v5 fixing analysis issues * feat(ui): stream attachment picker bottom sheet (#1336) * refactor(ui): improve stream_attachment_handler.dart, add stream_attachment_picker_bottom_sheet.dart Signed-off-by: xsahil03x * feat(ui): add copyWith in command_button.dart Signed-off-by: xsahil03x * chore(ui): remove permission_manager.dart Signed-off-by: xsahil03x * fix(ui): some fixes Signed-off-by: xsahil03x * fix(ui): more fixes Signed-off-by: xsahil03x * chore(ui): fix lints, add doc comments Signed-off-by: xsahil03x * chore(ci): update flutter version Signed-off-by: xsahil03x * chore(ui): more fixes Signed-off-by: xsahil03x * chore(ui): more fixes Signed-off-by: xsahil03x * chore(ui): more fixes Signed-off-by: xsahil03x * chore(ci): update melos and flutter version Signed-off-by: xsahil03x Signed-off-by: xsahil03x Co-authored-by: Salvatore Giordano * remove todo * update changelog * update changelogs * revert * update coverage threasholds Signed-off-by: xsahil03x Co-authored-by: GroovinChip Co-authored-by: Sahil Kumar Co-authored-by: Gordon Hayes Co-authored-by: Sacha Arbonel --- .github/workflows/dart_code_metrics.yaml | 4 +- .github/workflows/stream_flutter_workflow.yml | 19 +- CONTRIBUTING.md | 1 - analysis_options.yaml | 1 - .../guides/adding_push_notifications.mdx | 2 +- .../guides/adding_push_notifications_v2.mdx | 2 +- .../Flutter/guides/autocomplete_triggers.mdx | 118 + .../initialize_stream_chat_widget_tree.mdx | 696 - .../stream_user_list_controller.mdx | 3 +- .../guides/adding_push_notifications.mdx | 2 +- .../guides/adding_push_notifications_v2.mdx | 2 +- melos.yaml | 10 - packages/stream_chat/CHANGELOG.md | 32 + packages/stream_chat/example/lib/main.dart | 5 +- .../stream_chat/lib/src/client/channel.dart | 22 +- .../stream_chat/lib/src/client/client.dart | 24 +- .../lib/src/client/key_stroke_handler.dart | 1 + .../lib/src/core/api/requests.dart | 14 - .../lib/src/core/api/requests.g.dart | 4 - .../lib/src/core/api/stream_chat_api.dart | 6 +- .../lib/src/core/models/attachment.dart | 2 +- .../lib/src/core/models/attachment_file.dart | 19 +- .../lib/src/core/models/channel_model.dart | 1 - .../lib/src/core/models/member.dart | 7 - .../lib/src/core/models/member.g.dart | 2 - .../stream_chat/lib/src/core/models/user.dart | 1 - .../platform_detector/platform_detector.dart | 19 +- .../platform_detector_io.dart | 5 + .../platform_detector_stub.dart | 7 +- .../platform_detector_web.dart | 5 + .../lib/src/db/chat_persistence_client.dart | 16 +- packages/stream_chat/lib/stream_chat.dart | 4 + packages/stream_chat/lib/version.dart | 2 +- packages/stream_chat/pubspec.yaml | 2 +- .../test/fixtures/attachment_file.json | 5 + .../test/src/client/channel_test.dart | 12 +- .../test/src/client/retry_queue_test.dart | 2 - .../src/core/models/attachment_file_test.dart | 39 + packages/stream_chat_flutter/CHANGELOG.md | 110 +- packages/stream_chat_flutter/README.md | 20 + .../stream_chat_flutter/example/.gitignore | 1 - .../example/android/app/build.gradle | 2 +- .../android/app/src/main/AndroidManifest.xml | 3 + .../stream_chat_flutter/example/lib/main.dart | 303 +- .../example/lib/split_view.dart | 76 +- .../example/lib/tutorial_part_1.dart | 28 +- .../example/lib/tutorial_part_2.dart | 56 +- .../example/lib/tutorial_part_3.dart | 1 - .../example/lib/tutorial_part_4.dart | 30 +- .../example/lib/tutorial_part_5.dart | 31 +- .../example/lib/tutorial_part_6.dart | 60 +- .../example/linux/.gitignore | 1 + .../example/linux/CMakeLists.txt | 116 + .../example/linux/flutter/CMakeLists.txt | 87 + .../flutter/generated_plugin_registrant.cc | 35 + .../flutter/generated_plugin_registrant.h | 15 + .../linux/flutter/generated_plugins.cmake | 29 + .../stream_chat_flutter/example/linux/main.cc | 6 + .../example/linux/my_application.cc | 104 + .../example/linux/my_application.h | 18 + .../macos/Runner/DebugProfile.entitlements | 4 + .../example/macos/Runner/Info.plist | 2 + .../macos/Runner/MainFlutterWindow.swift | 4 +- .../stream_chat_flutter/example/pubspec.yaml | 3 +- .../example/web/icons/Icon-maskable-192.png | Bin 0 -> 5594 bytes .../example/web/icons/Icon-maskable-512.png | Bin 0 -> 20998 bytes .../example/web/index.html | 3 +- .../example/windows/.gitignore | 17 + .../example/windows/CMakeLists.txt | 95 + .../example/windows/flutter/CMakeLists.txt | 103 + .../flutter/generated_plugin_registrant.cc | 41 + .../flutter/generated_plugin_registrant.h | 15 + .../windows/flutter/generated_plugins.cmake | 33 + .../example/windows/runner/CMakeLists.txt | 17 + .../example/windows/runner/Runner.rc | 121 + .../example/windows/runner/flutter_window.cpp | 61 + .../example/windows/runner/flutter_window.h | 33 + .../example/windows/runner/main.cpp | 43 + .../example/windows/runner/resource.h | 16 + .../windows/runner/resources/app_icon.ico | Bin 0 -> 33772 bytes .../windows/runner/runner.exe.manifest | 20 + .../example/windows/runner/utils.cpp | 64 + .../example/windows/runner/utils.h | 19 + .../example/windows/runner/win32_window.cpp | 245 + .../example/windows/runner/win32_window.h | 98 + .../lib/conditional_parent_builder/README.md | 25 + .../conditional_parent_builder.dart | 55 + .../lib/platform_widget_builder/README.md | 28 + .../platform_widget_builder.dart | 4 + .../src/desktop_widget.dart | 38 + .../src/desktop_widget_base.dart | 53 + .../src/desktop_widget_builder.dart | 51 + .../src/platform_widget.dart | 38 + .../src/platform_widget_base.dart | 53 + .../src/platform_widget_builder.dart | 50 + .../src/item_positions_listener.dart | 1 - .../src/item_positions_notifier.dart | 1 - .../src/positioned_list.dart | 1 - .../src/scroll_view.dart | 1 - .../src/scrollable_positioned_list.dart | 1 - .../lib/src/attachment/attachment.dart | 4 +- .../lib/src/attachment/attachment_error.dart | 33 + .../lib/src/attachment/attachment_title.dart | 8 +- .../attachment_upload_state_builder.dart | 119 +- .../lib/src/attachment/attachment_widget.dart | 49 +- .../lib/src/attachment/file_attachment.dart | 225 +- .../lib/src/attachment/giphy_attachment.dart | 540 +- .../lib/src/attachment/handler/common.dart | 71 + .../handler/stream_attachment_handler.dart | 4 + .../stream_attachment_handler_base.dart | 80 + .../stream_attachment_handler_html.dart | 63 + .../handler/stream_attachment_handler_io.dart | 216 + .../lib/src/attachment/image_attachment.dart | 217 +- .../lib/src/attachment/image_group.dart | 175 + .../stream_attachment_package.dart | 2 +- .../lib/src/attachment/url_attachment.dart | 160 +- .../lib/src/attachment/video_attachment.dart | 175 +- .../attachment_actions_modal.dart | 194 +- .../src/autocomplete/stream_autocomplete.dart | 642 + .../stream_command_autocomplete_options.dart | 176 + .../stream_mention_autocomplete_options.dart} | 100 +- .../src/{ => avatars}/gradient_avatar.dart | 52 +- .../lib/src/{ => avatars}/group_avatar.dart | 21 +- .../lib/src/{ => avatars}/user_avatar.dart | 41 +- .../lib/src/back_button.dart | 59 - .../bottom_sheets/attachment_modal_sheet.dart | 68 + .../src/bottom_sheets/edit_message_sheet.dart | 95 + .../src/bottom_sheets/error_alert_sheet.dart | 76 + .../stream_channel_info_bottom_sheet.dart | 11 +- .../lib/src/{ => channel}/channel_header.dart | 59 +- .../lib/src/channel/channel_info.dart | 200 + .../{ => channel}/channel_list_header.dart | 134 +- .../lib/src/channel/channel_name.dart | 107 + .../lib/src/channel/channel_preview.dart | 508 + .../stream_channel_avatar.dart | 1 - .../{v4 => channel}/stream_channel_name.dart | 2 +- .../stream_message_preview_text.dart | 2 +- .../lib/src/channel_avatar.dart | 203 - .../lib/src/channel_bottom_sheet.dart | 266 - .../lib/src/channel_info.dart | 160 - .../lib/src/channel_list_view.dart | 742 - .../lib/src/channel_name.dart | 92 - .../lib/src/channel_preview.dart | 315 - .../lib/src/commands_overlay.dart | 221 - .../context_menu_reaction_picker.dart | 176 + .../download_menu_item.dart | 33 + .../stream_chat_context_menu_item.dart | 51 + .../lib/src/dialogs/channel_info_dialog.dart | 85 + .../lib/src/dialogs/confirmation_dialog.dart | 61 + .../src/dialogs/delete_message_dialog.dart | 43 + .../lib/src/dialogs/dialogs.dart | 4 + .../lib/src/dialogs/message_dialog.dart | 52 + .../lib/src/emoji/emoji.dart | 114494 --------------- .../lib/src/emoji_overlay.dart | 124 - .../lib/src/full_screen_media.dart | 353 - .../lib/src/fullscreen_media/fsm_enums.dart | 8 + .../lib/src/fullscreen_media/fsm_stub.dart | 19 + .../fullscreen_media/full_screen_media.dart | 493 + .../full_screen_media_builder.dart | 86 + .../full_screen_media_desktop.dart | 520 + .../full_screen_media_widget.dart | 10 + .../lib/src/{ => gallery}/gallery_footer.dart | 203 +- .../lib/src/{ => gallery}/gallery_header.dart | 54 +- .../lib/src/image_group.dart | 176 - .../{ => indicators}/sending_indicator.dart | 12 +- .../{ => indicators}/typing_indicator.dart | 17 +- .../{ => indicators}/unread_indicator.dart | 10 +- .../upload_progress_indicator.dart | 10 +- .../lib/src/keyboard_shortcuts/intents.dart | 13 + .../keyboard_shortcut_runner.dart | 60 + .../lib/src/keyboard_shortcuts/keysets.dart | 32 + .../stream_chat_localizations.dart | 12 +- .../lib/src/localization/translations.dart | 95 +- .../lib/src/media_list_view.dart | 320 - .../lib/src/media_list_view_controller.dart | 16 - .../lib/src/message_actions_modal.dart | 689 - .../copy_message_button.dart | 43 + .../delete_message_button.dart | 50 + .../edit_message_button.dart | 42 + .../flag_message_button.dart | 42 + .../message_actions_modal/mam_widgets.dart | 8 + .../message_action.dart | 12 +- .../message_actions_modal.dart | 516 + .../pin_message_button.dart | 49 + .../message_actions_modal/reply_button.dart | 42 + .../resend_message_button.dart | 56 + .../thread_reply_button.dart | 51 + .../lib/src/message_input.dart | 2086 - .../src/message_input/attachment_button.dart | 49 + .../attachment_picker/options/options.dart | 4 + .../options/stream_file_picker.dart | 115 + .../options/stream_gallery_picker.dart | 270 + .../options/stream_image_picker.dart | 96 + .../options/stream_video_picker.dart | 81 + .../stream_attachment_picker.dart | 821 + ...stream_attachment_picker_bottom_sheet.dart | 242 + .../clear_input_item_button.dart | 46 + .../lib/src/message_input/command_button.dart | 49 + .../src/message_input/countdown_button.dart | 35 + .../lib/src/message_input/dm_checkbox.dart | 75 + .../lib/src/message_input/enums.dart | 35 + .../message_input/quoted_message_widget.dart | 392 + .../quoting_message_top_area.dart | 60 + .../message_input/simple_safe_area.dart | 0 .../message_input/stream_message_input.dart | 1251 +- .../stream_message_send_button.dart | 0 .../stream_message_text_field.dart | 115 +- .../lib/src/{v4 => }/message_input/tld.dart | 0 .../floating_date_divider.dart | 98 + .../message_list_view/loading_indicator.dart | 62 + .../message_list_view/message_details.dart | 36 + .../message_list_view.dart | 866 +- .../lib/src/message_list_view/mlv_utils.dart | 103 + .../message_list_view/thread_separator.dart | 36 + .../unread_messages_separator.dart | 38 + .../lib/src/message_search_item.dart | 183 - .../lib/src/message_search_list_view.dart | 327 - .../lib/src/message_widget.dart | 1524 - .../lib/src/message_widget/bottom_row.dart | 221 + .../{ => message_widget}/deleted_message.dart | 16 +- .../lib/src/message_widget/message_card.dart | 232 + .../{ => message_widget}/message_text.dart | 17 +- .../src/message_widget/message_widget.dart | 1109 + .../message_widget_content.dart | 461 + .../message_widget_content_components.dart | 9 + .../src/message_widget/parse_attachments.dart | 71 + .../src/message_widget/pinned_message.dart | 52 + .../src/message_widget/quoted_message.dart | 71 + .../reactions/desktop_reactions_builder.dart | 367 + .../reactions}/message_reactions_modal.dart | 30 +- .../reactions}/reaction_bubble.dart | 12 +- .../reactions/reaction_indicator.dart | 85 + .../reactions}/reaction_picker.dart | 33 +- .../sending_indicator_wrapper.dart | 98 + .../lib/src/message_widget/text_bubble.dart | 73 + .../src/message_widget/thread_painter.dart | 53 + .../message_widget/thread_participants.dart | 49 + .../message_widget/user_avatar_transform.dart | 54 + .../lib/src/message_widget/username.dart | 31 + .../lib/src/misc/back_button.dart | 63 + .../{ => misc}/connection_status_builder.dart | 10 +- .../lib/src/{ => misc}/date_divider.dart | 14 +- .../lib/src/{ => misc}/info_tile.dart | 12 +- .../lib/src/{ => misc}/option_list_tile.dart | 12 +- .../lib/src/{ => misc}/reaction_icon.dart | 10 +- .../src/misc/stream_neumorphic_button.dart | 44 + .../lib/src/misc/stream_svg_icon.dart | 1146 + .../lib/src/{ => misc}/swipeable.dart | 80 +- .../lib/src/{ => misc}/system_message.dart | 17 +- .../lib/src/{ => misc}/thread_header.dart | 46 +- .../lib/src/{ => misc}/visible_footnote.dart | 15 +- .../lib/src/multi_overlay.dart | 71 - .../lib/src/quoted_message_widget.dart | 292 - .../stream_channel_grid_tile.dart | 0 .../stream_channel_grid_view.dart | 17 +- .../stream_channel_list_tile.dart | 91 +- .../stream_channel_list_view.dart | 175 +- .../stream_member_grid_view.dart | 397 + .../stream_member_list_view.dart | 365 + .../stream_message_search_grid_view.dart | 11 +- .../stream_message_search_list_tile.dart | 2 +- .../stream_message_search_list_view.dart | 167 +- .../photo_gallery/stream_photo_gallery.dart | 417 + .../stream_photo_gallery_controller.dart | 87 + .../stream_photo_gallery_tile.dart | 259 + .../stream_scroll_view_empty_widget.dart | 2 +- .../stream_scroll_view_error_widget.dart | 4 +- ...am_scroll_view_indexed_widget_builder.dart | 0 .../stream_scroll_view_load_more_error.dart | 3 +- ...tream_scroll_view_load_more_indicator.dart | 0 .../stream_scroll_view_loading_widget.dart | 0 .../stream_user_grid_tile.dart | 0 .../stream_user_grid_view.dart | 10 +- .../stream_user_list_tile.dart | 17 +- .../stream_user_list_view.dart | 10 +- .../lib/src/stream_chat.dart | 84 +- .../lib/src/stream_chat_configuration.dart | 229 + .../lib/src/stream_neumorphic_button.dart | 40 - .../lib/src/stream_svg_icon.dart | 999 - .../lib/src/theme/avatar_theme.dart | 30 +- .../lib/src/theme/channel_header_theme.dart | 42 +- .../src/theme/channel_list_header_theme.dart | 40 +- .../src/theme/channel_list_view_theme.dart | 125 - .../lib/src/theme/channel_preview_theme.dart | 58 +- .../lib/src/theme/color_theme.dart | 110 +- .../lib/src/theme/gallery_footer_theme.dart | 96 +- .../lib/src/theme/gallery_header_theme.dart | 60 +- .../lib/src/theme/message_input_theme.dart | 111 +- .../src/theme/message_list_view_theme.dart | 36 +- .../theme/message_search_list_view_theme.dart | 126 - .../lib/src/theme/message_theme.dart | 96 +- .../src/{ => theme}/stream_chat_theme.dart | 151 +- .../lib/src/theme/text_theme.dart | 4 - .../lib/src/theme/themes.dart | 3 - .../lib/src/theme/user_list_view_theme.dart | 123 - .../lib/src/{ => user}/user_item.dart | 26 +- .../lib/src/{ => user}/user_mention_tile.dart | 11 +- .../lib/src/user_list_view.dart | 446 - .../lib/src/utils/device_segmentation.dart | 28 + .../{extension.dart => utils/extensions.dart} | 182 +- .../src/{utils.dart => utils/helpers.dart} | 170 +- .../lib/src/utils/typedefs.dart | 360 + .../lib/src/utils/utils.dart | 4 + .../v4/message_input/countdown_button.dart | 32 - .../stream_attachment_picker.dart | 642 - .../lib/src/video/video_service.dart | 83 + .../{ => video}/video_thumbnail_image.dart | 50 +- .../lib/src/video/vlc/vlc_manager.dart | 22 + .../src/video/vlc/vlc_manager_desktop.dart | 13 + .../lib/src/video/vlc/vlc_manager_web.dart | 14 + .../lib/src/video/vlc/vlc_stub.dart | 5 + .../lib/src/video_service.dart | 47 - .../lib/stream_chat_flutter.dart | 159 +- packages/stream_chat_flutter/pubspec.yaml | 23 +- .../conditional_parent_builder_test.dart | 48 + .../test/flutter_test_config.dart | 1 - .../desktop_widget_builder_test.dart | 77 + .../platform_widget_builder_test.dart | 74 + .../src/attachment/attachment_error_test.dart | 25 + .../attachment/attachment_handler_test.dart | 68 + .../src/attachment/attachment_title_test.dart | 42 + .../attachment_upload_state_builder_test.dart | 33 + .../file_attachment_test.dart} | 8 +- .../src/attachment/giphy_attachment_test.dart | 52 + .../src/attachment/image_attachment_test.dart | 54 + .../test/src/attachment/image_group_test.dart | 65 + .../src/attachment/url_attachment_test.dart | 44 + .../attachment_actions_modal_test.dart | 134 +- .../{ => avatars}/gradient_avatar_test.dart | 2 +- .../test/src/avatars/group_avatar_test.dart | 227 + .../test/src/avatars/user_avatar_test.dart | 112 + .../attachment_modal_sheet_test.dart | 139 + .../edit_message_sheet_test.dart | 93 + .../bottom_sheets/error_alert_sheet_test.dart | 99 + .../{ => channel}/channel_header_test.dart | 232 +- .../src/{ => channel}/channel_image_test.dart | 119 +- .../channel_list_header_test.dart | 2 +- .../src/{ => channel}/channel_name_test.dart | 50 +- .../{ => channel}/channel_preview_test.dart | 50 +- .../download_menu_item_test.dart | 52 + .../stream_chat_context_menu_item_test.dart | 52 + .../src/dialogs/channel_info_dialog_test.dart | 79 + .../src/dialogs/confirmation_dialog_test.dart | 68 + .../dialogs/delete_message_dialog_test.dart | 51 + .../test/src/dialogs/message_dialog_test.dart | 126 + .../test/src/emoji_test.dart | 79 - .../full_screen_media_test.dart | 49 +- .../test/src/gallery/gallery_footer_test.dart | 111 + .../test/src/gallery/gallery_header_test.dart | 115 + .../test/src/goldens/attachment_button_0.png | Bin 0 -> 24553 bytes .../src/goldens/attachment_modal_sheet_0.png | Bin 0 -> 49362 bytes .../test/src/goldens/clear_input_item_0.png | Bin 0 -> 24076 bytes .../test/src/goldens/command_button_0.png | Bin 0 -> 23274 bytes .../src/goldens/confirmation_dialog_0.png | Bin 0 -> 55230 bytes .../test/src/goldens/countdown_button_0.png | Bin 0 -> 24794 bytes .../src/goldens/delete_message_dialog_0.png | Bin 0 -> 68100 bytes .../test/src/goldens/dm_checkbox_0.png | Bin 0 -> 33581 bytes .../test/src/goldens/dm_checkbox_1.png | Bin 0 -> 33436 bytes .../test/src/goldens/dm_checkbox_2.png | Bin 0 -> 33252 bytes .../test/src/goldens/download_menu_item_0.png | Bin 0 -> 27778 bytes .../test/src/goldens/edit_message_sheet_0.png | Bin 0 -> 40338 bytes .../test/src/goldens/error_alert_sheet_0.png | Bin 0 -> 42263 bytes .../test/src/goldens/gallery_footer_0.png | Bin 0 -> 26296 bytes .../test/src/goldens/gallery_header_0.png | Bin 0 -> 36757 bytes .../test/src/goldens/group_avatar_0.png | Bin 0 -> 60051 bytes .../test/src/goldens/message_dialog_0.png | Bin 0 -> 43765 bytes .../test/src/goldens/message_dialog_1.png | Bin 0 -> 48105 bytes .../test/src/goldens/message_dialog_2.png | Bin 0 -> 38907 bytes .../test/src/goldens/message_text.png | Bin 10173 -> 10184 bytes .../test/src/goldens/send_button_0.png | Bin 0 -> 23796 bytes .../test/src/goldens/sending_indicator_0.png | Bin 0 -> 22883 bytes .../test/src/goldens/sending_indicator_1.png | Bin 0 -> 24082 bytes .../test/src/goldens/sending_indicator_2.png | Bin 0 -> 24082 bytes .../stream_chat_context_menu_item_0.png | Bin 0 -> 27654 bytes .../goldens/upload_progress_indicator_0.png | Bin 0 -> 27267 bytes .../goldens/upload_progress_indicator_1.png | Bin 0 -> 28011 bytes .../goldens/upload_progress_indicator_2.png | Bin 0 -> 28109 bytes .../test/src/goldens/user_avatar_0.png | Bin 0 -> 33579 bytes .../test/src/goldens/user_avatar_1.png | Bin 0 -> 33324 bytes .../test/src/image_footer_test.dart | 52 - .../indicators/sending_indicator_test.dart | 88 + .../typing_indicator_test.dart | 49 +- .../unread_indicator_test.dart | 10 +- .../upload_progress_indicator_test.dart | 167 + .../keyboard_shortcut_runner_test.dart | 58 + .../default_translations_test.dart | 1 - .../src/media_list_view_controller_test.dart | 37 - .../message_actions_modal_test.dart} | 62 +- .../message_input/attachment_button_test.dart | 52 + .../message_input/clear_input_item_test.dart | 52 + .../message_input/command_button_test.dart | 46 + .../message_input/countdown_button_test.dart | 40 + .../src/message_input/dm_checkbox_test.dart | 133 + .../message_input_test.dart | 74 +- .../floating_date_divider_test.dart | 32 + .../message_list_view_test.dart | 13 +- .../thread_separator_test.dart | 26 + .../message_reactions_modal_test.dart | 6 +- .../deleted_message_test.dart | 48 +- .../message_text_test.dart | 12 +- .../src/message_widget/username_test.dart | 23 + .../test/src/{ => misc}/back_button_test.dart | 34 +- .../src/{ => misc}/date_divider_test.dart | 18 +- .../test/src/{ => misc}/info_tile_test.dart | 50 +- .../src/{ => misc}/reaction_bubble_test.dart | 4 +- .../src/{ => misc}/system_message_test.dart | 26 +- .../src/{ => misc}/thread_header_test.dart | 38 +- .../stream_chat_flutter/test/src/mocks.dart | 23 +- .../stream_member_list_view_test.dart | 62 + .../test/src/simple_frame.dart | 18 +- .../src/stream_chat_configuration_test.dart | 47 + .../theme/channel_list_view_theme_test.dart | 125 - .../message_search_list_view_theme_test.dart | 137 - .../src/theme/user_list_view_theme_test.dart | 119 - .../test/src/{ => utils}/extension_test.dart | 19 +- .../test/{utils => test_utils}/golden.dart | 5 +- .../stream_chat_flutter_core/CHANGELOG.md | 26 +- .../example/lib/main.dart | 6 +- .../example/pubspec.yaml | 2 +- .../windows/flutter/generated_plugins.cmake | 8 + .../lib/src/channel_list_core.dart | 261 - .../lib/src/channels_bloc.dart | 253 - .../lib/src/message_search_bloc.dart | 170 - .../lib/src/message_search_list_core.dart | 236 - .../lib/src/paged_value_notifier.freezed.dart | 156 +- .../lib/src/stream_channel.dart | 86 +- .../src/stream_channel_list_controller.dart | 39 +- .../src/stream_member_list_controller.dart | 154 + .../src/stream_message_input_controller.dart | 163 +- .../lib/src/user_list_core.dart | 286 - .../lib/src/users_bloc.dart | 145 - .../lib/stream_chat_flutter_core.dart | 7 +- .../stream_chat_flutter_core/pubspec.yaml | 4 +- .../test/channel_list_core_test.dart | 526 - .../test/channels_bloc_test.dart | 948 - .../test/message_list_core_test.dart | 9 +- .../test/message_search_bloc_test.dart | 400 - .../test/message_search_list_core_test.dart | 571 - .../test/stream_channel_test.dart | 3 + .../test/user_list_core_test.dart | 538 - .../test/users_bloc_test.dart | 365 - .../stream_chat_localizations/CHANGELOG.md | 22 + .../example/lib/add_new_lang.dart | 143 +- .../example/lib/main.dart | 24 +- .../example/lib/override_lang.dart | 24 +- .../lib/src/stream_chat_localizations.dart | 25 +- .../lib/src/stream_chat_localizations_de.dart | 59 +- .../lib/src/stream_chat_localizations_en.dart | 58 +- .../lib/src/stream_chat_localizations_es.dart | 59 +- .../lib/src/stream_chat_localizations_fr.dart | 60 +- .../lib/src/stream_chat_localizations_hi.dart | 57 +- .../lib/src/stream_chat_localizations_it.dart | 58 +- .../lib/src/stream_chat_localizations_ja.dart | 57 +- .../lib/src/stream_chat_localizations_ko.dart | 56 +- .../lib/src/stream_chat_localizations_no.dart | 52 +- .../lib/src/stream_chat_localizations_pt.dart | 59 +- .../stream_chat_localizations/pubspec.yaml | 6 +- .../test/translations_test.dart | 22 +- packages/stream_chat_persistence/CHANGELOG.md | 10 +- .../example/android/.gitignore | 2 + .../example/android/app/build.gradle | 19 +- .../android/app/src/main/AndroidManifest.xml | 19 +- .../res/drawable-v21/launch_background.xml | 12 + .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/styles.xml | 10 +- .../example/android/build.gradle | 8 +- .../example/android/gradle.properties | 1 - .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../example/lib/main.dart | 146 +- .../example/linux/.gitignore | 1 + .../example/linux/CMakeLists.txt | 116 + .../example/linux/flutter/CMakeLists.txt | 87 + .../flutter/generated_plugin_registrant.cc | 15 + .../flutter/generated_plugin_registrant.h | 15 + .../linux/flutter/generated_plugins.cmake | 24 + .../example/linux/main.cc | 6 + .../example/linux/my_application.cc | 104 + .../example/linux/my_application.h | 18 + .../example/macos/.gitignore | 7 + .../macos/Flutter/Flutter-Debug.xcconfig | 2 + .../macos/Flutter/Flutter-Release.xcconfig | 2 + .../macos/Runner.xcodeproj/project.pbxproj | 632 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 87 + .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../example/macos/Runner/AppDelegate.swift | 9 + .../AppIcon.appiconset/Contents.json | 68 + .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 46993 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 3276 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 1429 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 5933 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1243 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 14800 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 1874 bytes .../macos/Runner/Base.lproj/MainMenu.xib | 343 + .../macos/Runner/Configs/AppInfo.xcconfig | 14 + .../macos/Runner/Configs/Debug.xcconfig | 2 + .../macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 14 + .../example/macos/Runner/Info.plist | 32 + .../macos/Runner/MainFlutterWindow.swift | 15 + .../example/macos/Runner/Release.entitlements | 8 + .../example/pubspec.yaml | 4 +- .../example/web/favicon.png | Bin 0 -> 917 bytes .../example/web/icons/Icon-192.png | Bin 0 -> 5292 bytes .../example/web/icons/Icon-512.png | Bin 0 -> 8252 bytes .../example/web/icons/Icon-maskable-192.png | Bin 0 -> 5594 bytes .../example/web/icons/Icon-maskable-512.png | Bin 0 -> 20998 bytes .../example/web/index.html | 105 + .../example/web/manifest.json | 35 + .../example/web/sql-wasm.js | 201 + .../example/web/sql-wasm.wasm | Bin 0 -> 1108328 bytes .../example/windows/.gitignore | 17 + .../example/windows/CMakeLists.txt | 95 + .../example/windows/flutter/CMakeLists.txt | 103 + .../flutter/generated_plugin_registrant.cc | 14 + .../flutter/generated_plugin_registrant.h | 15 + .../windows/flutter/generated_plugins.cmake | 24 + .../example/windows/runner/CMakeLists.txt | 17 + .../example/windows/runner/Runner.rc | 121 + .../example/windows/runner/flutter_window.cpp | 61 + .../example/windows/runner/flutter_window.h | 33 + .../example/windows/runner/main.cpp | 43 + .../example/windows/runner/resource.h | 16 + .../windows/runner/resources/app_icon.ico | Bin 0 -> 33772 bytes .../windows/runner/runner.exe.manifest | 20 + .../example/windows/runner/utils.cpp | 64 + .../example/windows/runner/utils.h | 19 + .../example/windows/runner/win32_window.cpp | 245 + .../example/windows/runner/win32_window.h | 98 + .../lib/src/db/drift_chat_database.g.dart | 37 - .../lib/src/db/shared/native_db.dart | 5 +- .../lib/src/db/shared/unsupported_db.dart | 3 +- .../lib/src/db/shared/web_db.dart | 25 +- .../lib/src/entity/members.dart | 4 - .../lib/src/mapper/member_mapper.dart | 3 - .../src/stream_chat_persistence_client.dart | 18 +- packages/stream_chat_persistence/pubspec.yaml | 4 +- .../test/src/dao/member_dao_test.dart | 4 +- .../test/src/mapper/member_mapper_test.dart | 4 +- 542 files changed, 28065 insertions(+), 136564 deletions(-) create mode 100644 docusaurus/docs/Flutter/guides/autocomplete_triggers.mdx delete mode 100644 docusaurus/docs/Flutter/guides/initialize_stream_chat_widget_tree.mdx create mode 100644 packages/stream_chat/test/fixtures/attachment_file.json create mode 100644 packages/stream_chat/test/src/core/models/attachment_file_test.dart create mode 100644 packages/stream_chat_flutter/example/linux/.gitignore create mode 100644 packages/stream_chat_flutter/example/linux/CMakeLists.txt create mode 100644 packages/stream_chat_flutter/example/linux/flutter/CMakeLists.txt create mode 100644 packages/stream_chat_flutter/example/linux/flutter/generated_plugin_registrant.cc create mode 100644 packages/stream_chat_flutter/example/linux/flutter/generated_plugin_registrant.h create mode 100644 packages/stream_chat_flutter/example/linux/flutter/generated_plugins.cmake create mode 100644 packages/stream_chat_flutter/example/linux/main.cc create mode 100644 packages/stream_chat_flutter/example/linux/my_application.cc create mode 100644 packages/stream_chat_flutter/example/linux/my_application.h create mode 100644 packages/stream_chat_flutter/example/web/icons/Icon-maskable-192.png create mode 100644 packages/stream_chat_flutter/example/web/icons/Icon-maskable-512.png create mode 100644 packages/stream_chat_flutter/example/windows/.gitignore create mode 100644 packages/stream_chat_flutter/example/windows/CMakeLists.txt create mode 100644 packages/stream_chat_flutter/example/windows/flutter/CMakeLists.txt create mode 100644 packages/stream_chat_flutter/example/windows/flutter/generated_plugin_registrant.cc create mode 100644 packages/stream_chat_flutter/example/windows/flutter/generated_plugin_registrant.h create mode 100644 packages/stream_chat_flutter/example/windows/flutter/generated_plugins.cmake create mode 100644 packages/stream_chat_flutter/example/windows/runner/CMakeLists.txt create mode 100644 packages/stream_chat_flutter/example/windows/runner/Runner.rc create mode 100644 packages/stream_chat_flutter/example/windows/runner/flutter_window.cpp create mode 100644 packages/stream_chat_flutter/example/windows/runner/flutter_window.h create mode 100644 packages/stream_chat_flutter/example/windows/runner/main.cpp create mode 100644 packages/stream_chat_flutter/example/windows/runner/resource.h create mode 100644 packages/stream_chat_flutter/example/windows/runner/resources/app_icon.ico create mode 100644 packages/stream_chat_flutter/example/windows/runner/runner.exe.manifest create mode 100644 packages/stream_chat_flutter/example/windows/runner/utils.cpp create mode 100644 packages/stream_chat_flutter/example/windows/runner/utils.h create mode 100644 packages/stream_chat_flutter/example/windows/runner/win32_window.cpp create mode 100644 packages/stream_chat_flutter/example/windows/runner/win32_window.h create mode 100644 packages/stream_chat_flutter/lib/conditional_parent_builder/README.md create mode 100644 packages/stream_chat_flutter/lib/conditional_parent_builder/conditional_parent_builder.dart create mode 100644 packages/stream_chat_flutter/lib/platform_widget_builder/README.md create mode 100644 packages/stream_chat_flutter/lib/platform_widget_builder/platform_widget_builder.dart create mode 100644 packages/stream_chat_flutter/lib/platform_widget_builder/src/desktop_widget.dart create mode 100644 packages/stream_chat_flutter/lib/platform_widget_builder/src/desktop_widget_base.dart create mode 100644 packages/stream_chat_flutter/lib/platform_widget_builder/src/desktop_widget_builder.dart create mode 100644 packages/stream_chat_flutter/lib/platform_widget_builder/src/platform_widget.dart create mode 100644 packages/stream_chat_flutter/lib/platform_widget_builder/src/platform_widget_base.dart create mode 100644 packages/stream_chat_flutter/lib/platform_widget_builder/src/platform_widget_builder.dart create mode 100644 packages/stream_chat_flutter/lib/src/attachment/attachment_error.dart create mode 100644 packages/stream_chat_flutter/lib/src/attachment/handler/common.dart create mode 100644 packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler.dart create mode 100644 packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_base.dart create mode 100644 packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_html.dart create mode 100644 packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_io.dart create mode 100644 packages/stream_chat_flutter/lib/src/attachment/image_group.dart rename packages/stream_chat_flutter/lib/src/{ => attachment}/stream_attachment_package.dart (89%) rename packages/stream_chat_flutter/lib/src/{ => attachment_actions_modal}/attachment_actions_modal.dart (71%) create mode 100644 packages/stream_chat_flutter/lib/src/autocomplete/stream_autocomplete.dart create mode 100644 packages/stream_chat_flutter/lib/src/autocomplete/stream_command_autocomplete_options.dart rename packages/stream_chat_flutter/lib/src/{user_mentions_overlay.dart => autocomplete/stream_mention_autocomplete_options.dart} (60%) rename packages/stream_chat_flutter/lib/src/{ => avatars}/gradient_avatar.dart (86%) rename packages/stream_chat_flutter/lib/src/{ => avatars}/group_avatar.dart (92%) rename packages/stream_chat_flutter/lib/src/{ => avatars}/user_avatar.dart (85%) delete mode 100644 packages/stream_chat_flutter/lib/src/back_button.dart create mode 100644 packages/stream_chat_flutter/lib/src/bottom_sheets/attachment_modal_sheet.dart create mode 100644 packages/stream_chat_flutter/lib/src/bottom_sheets/edit_message_sheet.dart create mode 100644 packages/stream_chat_flutter/lib/src/bottom_sheets/error_alert_sheet.dart rename packages/stream_chat_flutter/lib/src/{v4 => bottom_sheets}/stream_channel_info_bottom_sheet.dart (95%) rename packages/stream_chat_flutter/lib/src/{ => channel}/channel_header.dart (84%) create mode 100644 packages/stream_chat_flutter/lib/src/channel/channel_info.dart rename packages/stream_chat_flutter/lib/src/{ => channel}/channel_list_header.dart (74%) create mode 100644 packages/stream_chat_flutter/lib/src/channel/channel_name.dart create mode 100644 packages/stream_chat_flutter/lib/src/channel/channel_preview.dart rename packages/stream_chat_flutter/lib/src/{v4 => channel}/stream_channel_avatar.dart (99%) rename packages/stream_chat_flutter/lib/src/{v4 => channel}/stream_channel_name.dart (97%) rename packages/stream_chat_flutter/lib/src/{v4 => channel}/stream_message_preview_text.dart (98%) delete mode 100644 packages/stream_chat_flutter/lib/src/channel_avatar.dart delete mode 100644 packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart delete mode 100644 packages/stream_chat_flutter/lib/src/channel_info.dart delete mode 100644 packages/stream_chat_flutter/lib/src/channel_list_view.dart delete mode 100644 packages/stream_chat_flutter/lib/src/channel_name.dart delete mode 100644 packages/stream_chat_flutter/lib/src/channel_preview.dart delete mode 100644 packages/stream_chat_flutter/lib/src/commands_overlay.dart create mode 100644 packages/stream_chat_flutter/lib/src/context_menu_items/context_menu_reaction_picker.dart create mode 100644 packages/stream_chat_flutter/lib/src/context_menu_items/download_menu_item.dart create mode 100644 packages/stream_chat_flutter/lib/src/context_menu_items/stream_chat_context_menu_item.dart create mode 100644 packages/stream_chat_flutter/lib/src/dialogs/channel_info_dialog.dart create mode 100644 packages/stream_chat_flutter/lib/src/dialogs/confirmation_dialog.dart create mode 100644 packages/stream_chat_flutter/lib/src/dialogs/delete_message_dialog.dart create mode 100644 packages/stream_chat_flutter/lib/src/dialogs/dialogs.dart create mode 100644 packages/stream_chat_flutter/lib/src/dialogs/message_dialog.dart delete mode 100644 packages/stream_chat_flutter/lib/src/emoji/emoji.dart delete mode 100644 packages/stream_chat_flutter/lib/src/emoji_overlay.dart delete mode 100644 packages/stream_chat_flutter/lib/src/full_screen_media.dart create mode 100644 packages/stream_chat_flutter/lib/src/fullscreen_media/fsm_enums.dart create mode 100644 packages/stream_chat_flutter/lib/src/fullscreen_media/fsm_stub.dart create mode 100644 packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media.dart create mode 100644 packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media_builder.dart create mode 100644 packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media_desktop.dart create mode 100644 packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media_widget.dart rename packages/stream_chat_flutter/lib/src/{ => gallery}/gallery_footer.dart (60%) rename packages/stream_chat_flutter/lib/src/{ => gallery}/gallery_header.dart (79%) delete mode 100644 packages/stream_chat_flutter/lib/src/image_group.dart rename packages/stream_chat_flutter/lib/src/{ => indicators}/sending_indicator.dart (75%) rename packages/stream_chat_flutter/lib/src/{ => indicators}/typing_indicator.dart (82%) rename packages/stream_chat_flutter/lib/src/{ => indicators}/unread_indicator.dart (85%) rename packages/stream_chat_flutter/lib/src/{ => indicators}/upload_progress_indicator.dart (86%) create mode 100644 packages/stream_chat_flutter/lib/src/keyboard_shortcuts/intents.dart create mode 100644 packages/stream_chat_flutter/lib/src/keyboard_shortcuts/keyboard_shortcut_runner.dart create mode 100644 packages/stream_chat_flutter/lib/src/keyboard_shortcuts/keysets.dart delete mode 100644 packages/stream_chat_flutter/lib/src/media_list_view.dart delete mode 100644 packages/stream_chat_flutter/lib/src/media_list_view_controller.dart delete mode 100644 packages/stream_chat_flutter/lib/src/message_actions_modal.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_actions_modal/copy_message_button.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_actions_modal/delete_message_button.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_actions_modal/edit_message_button.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_actions_modal/flag_message_button.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_actions_modal/mam_widgets.dart rename packages/stream_chat_flutter/lib/src/{ => message_actions_modal}/message_action.dart (51%) create mode 100644 packages/stream_chat_flutter/lib/src/message_actions_modal/message_actions_modal.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_actions_modal/pin_message_button.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_actions_modal/reply_button.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_actions_modal/resend_message_button.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_actions_modal/thread_reply_button.dart delete mode 100644 packages/stream_chat_flutter/lib/src/message_input.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_input/attachment_button.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/options.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_file_picker.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_gallery_picker.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_image_picker.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_video_picker.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_bottom_sheet.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_input/clear_input_item_button.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_input/command_button.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_input/dm_checkbox.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_input/enums.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_input/quoting_message_top_area.dart rename packages/stream_chat_flutter/lib/src/{v4 => }/message_input/simple_safe_area.dart (100%) rename packages/stream_chat_flutter/lib/src/{v4 => }/message_input/stream_message_input.dart (55%) rename packages/stream_chat_flutter/lib/src/{v4 => }/message_input/stream_message_send_button.dart (100%) rename packages/stream_chat_flutter/lib/src/{v4 => }/message_input/stream_message_text_field.dart (91%) rename packages/stream_chat_flutter/lib/src/{v4 => }/message_input/tld.dart (100%) create mode 100644 packages/stream_chat_flutter/lib/src/message_list_view/floating_date_divider.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_list_view/loading_indicator.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_list_view/message_details.dart rename packages/stream_chat_flutter/lib/src/{ => message_list_view}/message_list_view.dart (71%) create mode 100644 packages/stream_chat_flutter/lib/src/message_list_view/mlv_utils.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_list_view/thread_separator.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_list_view/unread_messages_separator.dart delete mode 100644 packages/stream_chat_flutter/lib/src/message_search_item.dart delete mode 100644 packages/stream_chat_flutter/lib/src/message_search_list_view.dart delete mode 100644 packages/stream_chat_flutter/lib/src/message_widget.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_widget/bottom_row.dart rename packages/stream_chat_flutter/lib/src/{ => message_widget}/deleted_message.dart (80%) create mode 100644 packages/stream_chat_flutter/lib/src/message_widget/message_card.dart rename packages/stream_chat_flutter/lib/src/{ => message_widget}/message_text.dart (87%) create mode 100644 packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_widget/message_widget_content_components.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_widget/parse_attachments.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_widget/pinned_message.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_widget/quoted_message.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_widget/reactions/desktop_reactions_builder.dart rename packages/stream_chat_flutter/lib/src/{ => message_widget/reactions}/message_reactions_modal.dart (91%) rename packages/stream_chat_flutter/lib/src/{ => message_widget/reactions}/reaction_bubble.dart (96%) create mode 100644 packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_indicator.dart rename packages/stream_chat_flutter/lib/src/{ => message_widget/reactions}/reaction_picker.dart (87%) create mode 100644 packages/stream_chat_flutter/lib/src/message_widget/sending_indicator_wrapper.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_widget/text_bubble.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_widget/thread_painter.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_widget/thread_participants.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_widget/user_avatar_transform.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_widget/username.dart create mode 100644 packages/stream_chat_flutter/lib/src/misc/back_button.dart rename packages/stream_chat_flutter/lib/src/{ => misc}/connection_status_builder.dart (83%) rename packages/stream_chat_flutter/lib/src/{ => misc}/date_divider.dart (80%) rename packages/stream_chat_flutter/lib/src/{ => misc}/info_tile.dart (83%) rename packages/stream_chat_flutter/lib/src/{ => misc}/option_list_tile.dart (88%) rename packages/stream_chat_flutter/lib/src/{ => misc}/reaction_icon.dart (63%) create mode 100644 packages/stream_chat_flutter/lib/src/misc/stream_neumorphic_button.dart create mode 100644 packages/stream_chat_flutter/lib/src/misc/stream_svg_icon.dart rename packages/stream_chat_flutter/lib/src/{ => misc}/swipeable.dart (72%) rename packages/stream_chat_flutter/lib/src/{ => misc}/system_message.dart (66%) rename packages/stream_chat_flutter/lib/src/{ => misc}/thread_header.dart (84%) rename packages/stream_chat_flutter/lib/src/{ => misc}/visible_footnote.dart (71%) delete mode 100644 packages/stream_chat_flutter/lib/src/multi_overlay.dart delete mode 100644 packages/stream_chat_flutter/lib/src/quoted_message_widget.dart rename packages/stream_chat_flutter/lib/src/{v4 => }/scroll_view/channel_scroll_view/stream_channel_grid_tile.dart (100%) rename packages/stream_chat_flutter/lib/src/{v4 => }/scroll_view/channel_scroll_view/stream_channel_grid_view.dart (94%) rename packages/stream_chat_flutter/lib/src/{v4 => }/scroll_view/channel_scroll_view/stream_channel_list_tile.dart (81%) rename packages/stream_chat_flutter/lib/src/{v4 => }/scroll_view/channel_scroll_view/stream_channel_list_view.dart (75%) create mode 100644 packages/stream_chat_flutter/lib/src/scroll_view/member_scroll_view/stream_member_grid_view.dart create mode 100644 packages/stream_chat_flutter/lib/src/scroll_view/member_scroll_view/stream_member_list_view.dart rename packages/stream_chat_flutter/lib/src/{v4 => }/scroll_view/message_search_scroll_view/stream_message_search_grid_view.dart (96%) rename packages/stream_chat_flutter/lib/src/{v4 => }/scroll_view/message_search_scroll_view/stream_message_search_list_tile.dart (99%) rename packages/stream_chat_flutter/lib/src/{v4 => }/scroll_view/message_search_scroll_view/stream_message_search_list_view.dart (76%) create mode 100644 packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery.dart create mode 100644 packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery_controller.dart create mode 100644 packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery_tile.dart rename packages/stream_chat_flutter/lib/src/{v4 => }/scroll_view/stream_scroll_view_empty_widget.dart (96%) rename packages/stream_chat_flutter/lib/src/{v4 => }/scroll_view/stream_scroll_view_error_widget.dart (95%) rename packages/stream_chat_flutter/lib/src/{v4 => }/scroll_view/stream_scroll_view_indexed_widget_builder.dart (100%) rename packages/stream_chat_flutter/lib/src/{v4 => }/scroll_view/stream_scroll_view_load_more_error.dart (96%) rename packages/stream_chat_flutter/lib/src/{v4 => }/scroll_view/stream_scroll_view_load_more_indicator.dart (100%) rename packages/stream_chat_flutter/lib/src/{v4 => }/scroll_view/stream_scroll_view_loading_widget.dart (100%) rename packages/stream_chat_flutter/lib/src/{v4 => }/scroll_view/user_scroll_view/stream_user_grid_tile.dart (100%) rename packages/stream_chat_flutter/lib/src/{v4 => }/scroll_view/user_scroll_view/stream_user_grid_view.dart (97%) rename packages/stream_chat_flutter/lib/src/{v4 => }/scroll_view/user_scroll_view/stream_user_list_tile.dart (90%) rename packages/stream_chat_flutter/lib/src/{v4 => }/scroll_view/user_scroll_view/stream_user_list_view.dart (96%) create mode 100644 packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart delete mode 100644 packages/stream_chat_flutter/lib/src/stream_neumorphic_button.dart delete mode 100644 packages/stream_chat_flutter/lib/src/stream_svg_icon.dart delete mode 100644 packages/stream_chat_flutter/lib/src/theme/channel_list_view_theme.dart delete mode 100644 packages/stream_chat_flutter/lib/src/theme/message_search_list_view_theme.dart rename packages/stream_chat_flutter/lib/src/{ => theme}/stream_chat_theme.dart (71%) delete mode 100644 packages/stream_chat_flutter/lib/src/theme/user_list_view_theme.dart rename packages/stream_chat_flutter/lib/src/{ => user}/user_item.dart (77%) rename packages/stream_chat_flutter/lib/src/{ => user}/user_mention_tile.dart (89%) delete mode 100644 packages/stream_chat_flutter/lib/src/user_list_view.dart create mode 100644 packages/stream_chat_flutter/lib/src/utils/device_segmentation.dart rename packages/stream_chat_flutter/lib/src/{extension.dart => utils/extensions.dart} (70%) rename packages/stream_chat_flutter/lib/src/{utils.dart => utils/helpers.dart} (80%) create mode 100644 packages/stream_chat_flutter/lib/src/utils/typedefs.dart create mode 100644 packages/stream_chat_flutter/lib/src/utils/utils.dart delete mode 100644 packages/stream_chat_flutter/lib/src/v4/message_input/countdown_button.dart delete mode 100644 packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart create mode 100644 packages/stream_chat_flutter/lib/src/video/video_service.dart rename packages/stream_chat_flutter/lib/src/{ => video}/video_thumbnail_image.dart (76%) create mode 100644 packages/stream_chat_flutter/lib/src/video/vlc/vlc_manager.dart create mode 100644 packages/stream_chat_flutter/lib/src/video/vlc/vlc_manager_desktop.dart create mode 100644 packages/stream_chat_flutter/lib/src/video/vlc/vlc_manager_web.dart create mode 100644 packages/stream_chat_flutter/lib/src/video/vlc/vlc_stub.dart delete mode 100644 packages/stream_chat_flutter/lib/src/video_service.dart create mode 100644 packages/stream_chat_flutter/test/conditional_parent_builder/conditional_parent_builder_test.dart create mode 100644 packages/stream_chat_flutter/test/platform_widget_builder/desktop_widget_builder_test.dart create mode 100644 packages/stream_chat_flutter/test/platform_widget_builder/platform_widget_builder_test.dart create mode 100644 packages/stream_chat_flutter/test/src/attachment/attachment_error_test.dart create mode 100644 packages/stream_chat_flutter/test/src/attachment/attachment_handler_test.dart create mode 100644 packages/stream_chat_flutter/test/src/attachment/attachment_title_test.dart create mode 100644 packages/stream_chat_flutter/test/src/attachment/attachment_upload_state_builder_test.dart rename packages/stream_chat_flutter/test/src/{attachment_widgets_test.dart => attachment/file_attachment_test.dart} (90%) create mode 100644 packages/stream_chat_flutter/test/src/attachment/giphy_attachment_test.dart create mode 100644 packages/stream_chat_flutter/test/src/attachment/image_attachment_test.dart create mode 100644 packages/stream_chat_flutter/test/src/attachment/image_group_test.dart create mode 100644 packages/stream_chat_flutter/test/src/attachment/url_attachment_test.dart rename packages/stream_chat_flutter/test/src/{ => attachment_actions_modal}/attachment_actions_modal_test.dart (74%) rename packages/stream_chat_flutter/test/src/{ => avatars}/gradient_avatar_test.dart (99%) create mode 100644 packages/stream_chat_flutter/test/src/avatars/group_avatar_test.dart create mode 100644 packages/stream_chat_flutter/test/src/avatars/user_avatar_test.dart create mode 100644 packages/stream_chat_flutter/test/src/bottom_sheets/attachment_modal_sheet_test.dart create mode 100644 packages/stream_chat_flutter/test/src/bottom_sheets/edit_message_sheet_test.dart create mode 100644 packages/stream_chat_flutter/test/src/bottom_sheets/error_alert_sheet_test.dart rename packages/stream_chat_flutter/test/src/{ => channel}/channel_header_test.dart (78%) rename packages/stream_chat_flutter/test/src/{ => channel}/channel_image_test.dart (76%) rename packages/stream_chat_flutter/test/src/{ => channel}/channel_list_header_test.dart (99%) rename packages/stream_chat_flutter/test/src/{ => channel}/channel_name_test.dart (69%) rename packages/stream_chat_flutter/test/src/{ => channel}/channel_preview_test.dart (77%) create mode 100644 packages/stream_chat_flutter/test/src/context_menu_items/download_menu_item_test.dart create mode 100644 packages/stream_chat_flutter/test/src/context_menu_items/stream_chat_context_menu_item_test.dart create mode 100644 packages/stream_chat_flutter/test/src/dialogs/channel_info_dialog_test.dart create mode 100644 packages/stream_chat_flutter/test/src/dialogs/confirmation_dialog_test.dart create mode 100644 packages/stream_chat_flutter/test/src/dialogs/delete_message_dialog_test.dart create mode 100644 packages/stream_chat_flutter/test/src/dialogs/message_dialog_test.dart delete mode 100644 packages/stream_chat_flutter/test/src/emoji_test.dart rename packages/stream_chat_flutter/test/src/{ => full_screen_media}/full_screen_media_test.dart (75%) create mode 100644 packages/stream_chat_flutter/test/src/gallery/gallery_footer_test.dart create mode 100644 packages/stream_chat_flutter/test/src/gallery/gallery_header_test.dart create mode 100644 packages/stream_chat_flutter/test/src/goldens/attachment_button_0.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/attachment_modal_sheet_0.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/clear_input_item_0.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/command_button_0.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/confirmation_dialog_0.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/countdown_button_0.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/delete_message_dialog_0.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/dm_checkbox_0.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/dm_checkbox_1.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/dm_checkbox_2.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/download_menu_item_0.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/edit_message_sheet_0.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/error_alert_sheet_0.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/gallery_footer_0.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/gallery_header_0.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/group_avatar_0.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/message_dialog_0.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/message_dialog_1.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/message_dialog_2.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/send_button_0.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/sending_indicator_0.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/sending_indicator_1.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/sending_indicator_2.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/stream_chat_context_menu_item_0.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/upload_progress_indicator_0.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/upload_progress_indicator_1.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/upload_progress_indicator_2.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/user_avatar_0.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/user_avatar_1.png delete mode 100644 packages/stream_chat_flutter/test/src/image_footer_test.dart create mode 100644 packages/stream_chat_flutter/test/src/indicators/sending_indicator_test.dart rename packages/stream_chat_flutter/test/src/{ => indicators}/typing_indicator_test.dart (71%) rename packages/stream_chat_flutter/test/src/{ => indicators}/unread_indicator_test.dart (96%) create mode 100644 packages/stream_chat_flutter/test/src/indicators/upload_progress_indicator_test.dart create mode 100644 packages/stream_chat_flutter/test/src/keyboard_shortcuts/keyboard_shortcut_runner_test.dart rename packages/stream_chat_flutter/test/src/{ => localization}/default_translations_test.dart (99%) delete mode 100644 packages/stream_chat_flutter/test/src/media_list_view_controller_test.dart rename packages/stream_chat_flutter/test/src/{message_action_modal_test.dart => message_actions_modal/message_actions_modal_test.dart} (94%) create mode 100644 packages/stream_chat_flutter/test/src/message_input/attachment_button_test.dart create mode 100644 packages/stream_chat_flutter/test/src/message_input/clear_input_item_test.dart create mode 100644 packages/stream_chat_flutter/test/src/message_input/command_button_test.dart create mode 100644 packages/stream_chat_flutter/test/src/message_input/countdown_button_test.dart create mode 100644 packages/stream_chat_flutter/test/src/message_input/dm_checkbox_test.dart rename packages/stream_chat_flutter/test/src/{ => message_input}/message_input_test.dart (75%) create mode 100644 packages/stream_chat_flutter/test/src/message_list_view/floating_date_divider_test.dart rename packages/stream_chat_flutter/test/src/{ => message_list_view}/message_list_view_test.dart (96%) create mode 100644 packages/stream_chat_flutter/test/src/message_list_view/thread_separator_test.dart rename packages/stream_chat_flutter/test/src/{ => message_reactions_modal}/message_reactions_modal_test.dart (94%) rename packages/stream_chat_flutter/test/src/{ => message_widget}/deleted_message_test.dart (87%) rename packages/stream_chat_flutter/test/src/{ => message_widget}/message_text_test.dart (97%) create mode 100644 packages/stream_chat_flutter/test/src/message_widget/username_test.dart rename packages/stream_chat_flutter/test/src/{ => misc}/back_button_test.dart (82%) rename packages/stream_chat_flutter/test/src/{ => misc}/date_divider_test.dart (70%) rename packages/stream_chat_flutter/test/src/{ => misc}/info_tile_test.dart (58%) rename packages/stream_chat_flutter/test/src/{ => misc}/reaction_bubble_test.dart (98%) rename packages/stream_chat_flutter/test/src/{ => misc}/system_message_test.dart (92%) rename packages/stream_chat_flutter/test/src/{ => misc}/thread_header_test.dart (88%) create mode 100644 packages/stream_chat_flutter/test/src/scroll_view/member_scroll_view/stream_member_list_view_test.dart create mode 100644 packages/stream_chat_flutter/test/src/stream_chat_configuration_test.dart delete mode 100644 packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart delete mode 100644 packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart delete mode 100644 packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart rename packages/stream_chat_flutter/test/src/{ => utils}/extension_test.dart (77%) rename packages/stream_chat_flutter/test/{utils => test_utils}/golden.dart (88%) delete mode 100644 packages/stream_chat_flutter_core/lib/src/channel_list_core.dart delete mode 100644 packages/stream_chat_flutter_core/lib/src/channels_bloc.dart delete mode 100644 packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart delete mode 100644 packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart create mode 100644 packages/stream_chat_flutter_core/lib/src/stream_member_list_controller.dart delete mode 100644 packages/stream_chat_flutter_core/lib/src/user_list_core.dart delete mode 100644 packages/stream_chat_flutter_core/lib/src/users_bloc.dart delete mode 100644 packages/stream_chat_flutter_core/test/channel_list_core_test.dart delete mode 100644 packages/stream_chat_flutter_core/test/channels_bloc_test.dart delete mode 100644 packages/stream_chat_flutter_core/test/message_search_bloc_test.dart delete mode 100644 packages/stream_chat_flutter_core/test/message_search_list_core_test.dart delete mode 100644 packages/stream_chat_flutter_core/test/user_list_core_test.dart delete mode 100644 packages/stream_chat_flutter_core/test/users_bloc_test.dart create mode 100644 packages/stream_chat_persistence/example/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 packages/stream_chat_persistence/example/android/app/src/main/res/values-night/styles.xml create mode 100644 packages/stream_chat_persistence/example/linux/.gitignore create mode 100644 packages/stream_chat_persistence/example/linux/CMakeLists.txt create mode 100644 packages/stream_chat_persistence/example/linux/flutter/CMakeLists.txt create mode 100644 packages/stream_chat_persistence/example/linux/flutter/generated_plugin_registrant.cc create mode 100644 packages/stream_chat_persistence/example/linux/flutter/generated_plugin_registrant.h create mode 100644 packages/stream_chat_persistence/example/linux/flutter/generated_plugins.cmake create mode 100644 packages/stream_chat_persistence/example/linux/main.cc create mode 100644 packages/stream_chat_persistence/example/linux/my_application.cc create mode 100644 packages/stream_chat_persistence/example/linux/my_application.h create mode 100644 packages/stream_chat_persistence/example/macos/.gitignore create mode 100644 packages/stream_chat_persistence/example/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 packages/stream_chat_persistence/example/macos/Flutter/Flutter-Release.xcconfig create mode 100644 packages/stream_chat_persistence/example/macos/Runner.xcodeproj/project.pbxproj create mode 100644 packages/stream_chat_persistence/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/stream_chat_persistence/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 packages/stream_chat_persistence/example/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 packages/stream_chat_persistence/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/stream_chat_persistence/example/macos/Runner/AppDelegate.swift create mode 100644 packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 packages/stream_chat_persistence/example/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 packages/stream_chat_persistence/example/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 packages/stream_chat_persistence/example/macos/Runner/Configs/Debug.xcconfig create mode 100644 packages/stream_chat_persistence/example/macos/Runner/Configs/Release.xcconfig create mode 100644 packages/stream_chat_persistence/example/macos/Runner/Configs/Warnings.xcconfig create mode 100644 packages/stream_chat_persistence/example/macos/Runner/DebugProfile.entitlements create mode 100644 packages/stream_chat_persistence/example/macos/Runner/Info.plist create mode 100644 packages/stream_chat_persistence/example/macos/Runner/MainFlutterWindow.swift create mode 100644 packages/stream_chat_persistence/example/macos/Runner/Release.entitlements create mode 100644 packages/stream_chat_persistence/example/web/favicon.png create mode 100644 packages/stream_chat_persistence/example/web/icons/Icon-192.png create mode 100644 packages/stream_chat_persistence/example/web/icons/Icon-512.png create mode 100644 packages/stream_chat_persistence/example/web/icons/Icon-maskable-192.png create mode 100644 packages/stream_chat_persistence/example/web/icons/Icon-maskable-512.png create mode 100644 packages/stream_chat_persistence/example/web/index.html create mode 100644 packages/stream_chat_persistence/example/web/manifest.json create mode 100644 packages/stream_chat_persistence/example/web/sql-wasm.js create mode 100755 packages/stream_chat_persistence/example/web/sql-wasm.wasm create mode 100644 packages/stream_chat_persistence/example/windows/.gitignore create mode 100644 packages/stream_chat_persistence/example/windows/CMakeLists.txt create mode 100644 packages/stream_chat_persistence/example/windows/flutter/CMakeLists.txt create mode 100644 packages/stream_chat_persistence/example/windows/flutter/generated_plugin_registrant.cc create mode 100644 packages/stream_chat_persistence/example/windows/flutter/generated_plugin_registrant.h create mode 100644 packages/stream_chat_persistence/example/windows/flutter/generated_plugins.cmake create mode 100644 packages/stream_chat_persistence/example/windows/runner/CMakeLists.txt create mode 100644 packages/stream_chat_persistence/example/windows/runner/Runner.rc create mode 100644 packages/stream_chat_persistence/example/windows/runner/flutter_window.cpp create mode 100644 packages/stream_chat_persistence/example/windows/runner/flutter_window.h create mode 100644 packages/stream_chat_persistence/example/windows/runner/main.cpp create mode 100644 packages/stream_chat_persistence/example/windows/runner/resource.h create mode 100644 packages/stream_chat_persistence/example/windows/runner/resources/app_icon.ico create mode 100644 packages/stream_chat_persistence/example/windows/runner/runner.exe.manifest create mode 100644 packages/stream_chat_persistence/example/windows/runner/utils.cpp create mode 100644 packages/stream_chat_persistence/example/windows/runner/utils.h create mode 100644 packages/stream_chat_persistence/example/windows/runner/win32_window.cpp create mode 100644 packages/stream_chat_persistence/example/windows/runner/win32_window.h diff --git a/.github/workflows/dart_code_metrics.yaml b/.github/workflows/dart_code_metrics.yaml index 7e3e41fd..9162b1a7 100644 --- a/.github/workflows/dart_code_metrics.yaml +++ b/.github/workflows/dart_code_metrics.yaml @@ -1,9 +1,9 @@ name: Dart Code Metrics env: - flutter_version: "3.0.0" + flutter_version: "3.3.3" folders: "lib, test" - melos_version: "2.1.0" + melos_version: "2.7.1" on: pull_request: diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index 8b2d2481..e24582e7 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -2,8 +2,8 @@ name: stream_flutter_workflow env: ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' - flutter_version: "3.0.0" - melos_version: "2.1.0" + flutter_version: "3.3.3" + melos_version: "2.7.1" on: pull_request: @@ -77,7 +77,7 @@ jobs: test: runs-on: macos-latest if: github.event.pull_request.draft == false - timeout-minutes: 20 + timeout-minutes: 30 steps: - name: "Git Checkout" uses: actions/checkout@v2 @@ -111,22 +111,27 @@ jobs: uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 with: path: packages/stream_chat/coverage/lcov.info - min_coverage: 80 + min_coverage: 79 + - name: "Stream Chat Persistence Coverage Check" + uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 + with: + path: packages/stream_chat_localizations/coverage/lcov.info + min_coverage: 88 - name: "Stream Chat Persistence Coverage Check" uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 with: path: packages/stream_chat_persistence/coverage/lcov.info - min_coverage: 95 + min_coverage: 97 - name: "Stream Chat Flutter Core Coverage Check" uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 with: path: packages/stream_chat_flutter_core/coverage/lcov.info - min_coverage: 90 + min_coverage: 30 - name: "Stream Chat Flutter Coverage Check" uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 with: path: packages/stream_chat_flutter/coverage/lcov.info - min_coverage: 67 + min_coverage: 44 draft-build: runs-on: ubuntu-latest diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 76a09d70..385b6085 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -198,7 +198,6 @@ analyzer: exclude: - packages/*/lib/**/*.g.dart - packages/*/example/** - - packages/*/lib/src/emoji - packages/*/lib/**/*.freezed.dart - packages/*/test/** diff --git a/analysis_options.yaml b/analysis_options.yaml index 52f50e45..6386f168 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -3,7 +3,6 @@ analyzer: - dart_code_metrics exclude: - packages/*/lib/**/*.g.dart - - packages/*/lib/src/emoji/** - packages/*/lib/scrollable_positioned_list/** - packages/*/lib/**/*.freezed.dart diff --git a/docusaurus/docs/Flutter/guides/adding_push_notifications.mdx b/docusaurus/docs/Flutter/guides/adding_push_notifications.mdx index e845c3c6..328ba546 100644 --- a/docusaurus/docs/Flutter/guides/adding_push_notifications.mdx +++ b/docusaurus/docs/Flutter/guides/adding_push_notifications.mdx @@ -128,7 +128,7 @@ If you're not sure if you've set up push notifications correctly (e.g. you don't 11. After configuring [stream-cli](https://github.com/GetStream/stream-cli) paste the following command on command line using your user ID ```shell -stream-cli chat test-push --user-id +stream chat:push:test -u ``` You should get a test push notification diff --git a/docusaurus/docs/Flutter/guides/adding_push_notifications_v2.mdx b/docusaurus/docs/Flutter/guides/adding_push_notifications_v2.mdx index a03efe9a..917d9216 100644 --- a/docusaurus/docs/Flutter/guides/adding_push_notifications_v2.mdx +++ b/docusaurus/docs/Flutter/guides/adding_push_notifications_v2.mdx @@ -223,7 +223,7 @@ If you're not sure whether you've set up push notifications correctly, for examp 9. Tap on `Device ID` and copy it 11. After configuring [stream-cli](https://github.com/GetStream/stream-cli), run the following command using your user ID: ```shell -stream-cli chat test-push --user-id +stream chat:push:test -u ``` You should get a test push notification 🥳 diff --git a/docusaurus/docs/Flutter/guides/autocomplete_triggers.mdx b/docusaurus/docs/Flutter/guides/autocomplete_triggers.mdx new file mode 100644 index 00000000..413d147a --- /dev/null +++ b/docusaurus/docs/Flutter/guides/autocomplete_triggers.mdx @@ -0,0 +1,118 @@ +--- +id: autocomplete_triggers +title: Adding Custom Autocomplete Triggers +--- + +Adding Custom Autocomplete Triggers + +### Introduction + +The [StreamMessageInput](../stream_chat_flutter/message_input.mdx) widget provides a way to add custom autocomplete triggers using the `StreamMessageInput.customAutocompleteTriggers` property. + +By default we provide autocomplete triggers for mentions and commands, but it's very easy to add your custom ones. + +### Add Emoji Autocomplete Trigger + +To add a custom emoji autocomplete trigger, you must first create an `AutoCompleteOptions` widget. +This widget will be used to show the autocomplete options. + +For this example we're using two external dependencies: + +- [emojis](https://pub.dev/packages/emojis) +- [substring_highlight](https://pub.dev/packages/substring_highlight) + +```dart +import 'package:emojis/emoji.dart'; +import 'package:flutter/material.dart'; + +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:substring_highlight/substring_highlight.dart'; + +/// Overlay for displaying emoji that can be used +class StreamEmojiAutocompleteOptions extends StatelessWidget { + /// Constructor for creating a [StreamEmojiAutocompleteOptions] + const StreamEmojiAutocompleteOptions({ + super.key, + required this.query, + this.onEmojiSelected, + }); + + /// Query for searching emoji. + final String query; + + /// Callback called when an emoji is selected. + final ValueSetter? onEmojiSelected; + + @override + Widget build(BuildContext context) { + final emojis = Emoji.all().where((it) { + final normalizedQuery = query.toUpperCase(); + final normalizedShortName = it.shortName.toUpperCase(); + + return normalizedShortName.contains(normalizedQuery); + }); + + if (emojis.isEmpty) return const SizedBox.shrink(); + + return StreamAutocompleteOptions( + options: emojis, + optionBuilder: (context, emoji) { + final themeData = Theme.of(context); + return ListTile( + dense: true, + horizontalTitleGap: 0, + leading: Text( + emoji.char, + style: themeData.textTheme.headline6!.copyWith( + fontSize: 24, + ), + ), + title: SubstringHighlight( + text: emoji.shortName, + term: query, + textStyleHighlight: themeData.textTheme.headline6!.copyWith( + color: Colors.yellow, + fontSize: 14.5, + fontWeight: FontWeight.bold, + ), + textStyle: themeData.textTheme.headline6!.copyWith( + fontSize: 14.5, + ), + ), + onTap: onEmojiSelected == null ? null : () => onEmojiSelected!(emoji), + ); + }, + ); + } +} +``` + +Now it's time to use the `StreamEmojiAutocompleteOptions` widget. + +```dart +StreamMessageInput( + customAutocompleteTriggers: [ + StreamAutocompleteTrigger( + trigger: ':', + minimumRequiredCharacters: 2, + optionsViewBuilder: ( + context, + autocompleteQuery, + messageEditingController, + ) { + final query = autocompleteQuery.query; + return StreamEmojiAutocompleteOptions( + query: query, + onEmojiSelected: (emoji) { + // accepting the autocomplete option. + StreamAutocomplete.of(context).acceptAutocompleteOption( + emoji.char, + keepTrigger: false, + ); + }, + ); + }, + ), + ], +), +``` diff --git a/docusaurus/docs/Flutter/guides/initialize_stream_chat_widget_tree.mdx b/docusaurus/docs/Flutter/guides/initialize_stream_chat_widget_tree.mdx deleted file mode 100644 index 13ad048d..00000000 --- a/docusaurus/docs/Flutter/guides/initialize_stream_chat_widget_tree.mdx +++ /dev/null @@ -1,696 +0,0 @@ ---- -id: initialize_stream_chat_widget_tree -sidebar_position: 15 -title: Initialize Stream Chat in Part of the Widget Tree ---- - -If you’re creating a full-scale chat application, you probably want to have Stream Chat Flutter initialized at the top of your widget tree and the Stream user connected as soon as they open the application. - -However, if you only need chat functionality in a part of your application, then it’ll be better to delay Stream Chat initialization to when it’s needed. -This guide demonstrates three alternative ways for you to initialize Stream Chat Flutter for a part of your widget tree and to only connect a user when needed. - -## What To Keep In Mind? - -Before investigating potential solutions, let’s first take a look at the relevant Stream Chat widgets and classes. - -Most of the Stream Chat Flutter UI widgets rely on having a [StreamChat](https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter/stream_chat_and_theming/) ancestor in the widget tree. -The **StreamChat** widget is an [InheritedWidget](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html) that exposes the **StreamChatClient** through **BuildContext**. -This widget also initializes the [StreamChatCore](../stream_chat_flutter_core/stream_chat_core.mdx) widget and the **StreamChatTheme**. - -**StreamChatCore** is a **StatefulWidget** used to react to life cycle changes and system updates. -When the app goes into the background, the WebSocket connection is closed. -Conversely, a new connection is initiated when the app is back in the foreground. - -What is important to take note of is that **a connection is only established if a user is connected**. - -This means that if you have not yet called `client.connectUser(user, token)`, no connection will be made, and only background listeners will be registered to determine the app's foreground state. - -## Option 1: Builder and Connect/Disconnect User - -This option requires you to wrap your whole application with the **StreamChat** widget and to call `connectUser` and `disconnectUser` as needed. - -This option is the easiest, however, it requires **StreamChat** to be at the top of each route and as a result, will have a slight overhead as it’ll create the above-mentioned Stream widgets that may not yet be needed. - -### Exposing the StreamChat Widget - -First, you must expose the client and base Stream Chat widgets to the whole application. - -```dart -void main() { - final client = StreamChatClient( - 'q29npdvqjr99', - logLevel: Level.OFF, - ); - - runApp(MyApp(client: client)); -} - -class MyApp extends StatelessWidget { - const MyApp({ - Key? key, - required this.client, - }) : super(key: key); - - final StreamChatClient client; - - @override - Widget build(BuildContext context) { - return MaterialApp( - builder: (context, child) { - return StreamChat(client: client, child: child); - }, - home: const HomeScreen(), - ); - } -} -``` - -In the above code, you: - -1. Create a **StreamChatClient** instance -2. Pass the instance to the **StreamChat** widget -3. Expose **StreamChat** to the whole application within the **MaterialApp** `builder` - -A few important things to note: - -- The `builder` wraps the **StreamChat** widget for every route of our application. No matter where you are in the widget tree, you’ll be able to call `StreamChat.of(context)`. -- The state will only be created once for our application, as **StreamChat** is a **StatefulWidget** and the position of **StreamChat** in the widget tree remains the same throughout the application lifecycle. -- No connection will be made until you call `connectUser`. - -### Connecting and Disconnecting Users - -In **MaterialApp** above, the home page is set to **HomeScreen**. This screen could look something like the following: - -```dart -class HomeScreen extends StatelessWidget { - const HomeScreen({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(), - body: const Center( - child: Text('Home Screen'), - ), - floatingActionButton: FloatingActionButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => const ChatSetup())); - }, - child: const Icon( - Icons.message, - ), - ), - ); - } -} -``` - -This screen shows a floating action button that on click navigates the user to the **ChatSetup**. We want to connect and disconnect a Stream user only when they go to the chat setup screen. - -```dart -class ChatSetup extends StatefulWidget { - const ChatSetup({ - super.key, - }); - - @override - State createState() => _ChatSetupState(); -} - -class _ChatSetupState extends State { - late final Future connectionFuture; - late final client = StreamChat.of(context).client; - - @override - void initState() { - super.initState(); - connectionFuture = client.connectUser( - User(id: 'USER_ID'), - 'TOKEN', - ); - } - - @override - void dispose() { - client.disconnectUser(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(), - body: FutureBuilder( - future: connectionFuture, - builder: (BuildContext context, AsyncSnapshot snapshot) { - switch (snapshot.connectionState) { - case ConnectionState.waiting: - return const Center( - child: CircularProgressIndicator(), - ); - default: - if (snapshot.hasError) { - return Text('Error: ${snapshot.error}'); - } else { - return const ChannelListPage(); - } - } - }, - ), - ); - } -} -``` - -Within `initState` you call `connectUser`; once the future for `connectUser` has completed a connection to the Stream API is established, you can then display the relevant Stream Chat UI widgets. - -Once the **ChatScreen** widget is disposed of, then `disconnectUser` will be called within the `dispose` method. - -### Caution - -In this example, `disconnectUser` will **only** be called when the **ChatSetup** widget is disposed. - -You need to ensure that this widget (route) is completely disposed of, or you need to call `disconnectUser` and `connectUser` manually when navigating to relevant parts of your application. - -For example, let’s say you have a button within one of the chat screens to navigate to a completely different part of your app, then you want to make sure the **ChatScreen** route is disposed of by forcing it to be removed: - -```dart -Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute( - builder: ((context) => const HomeScreen()), - ), - (Route route) => false, -); -``` - -Or let’s say you wanted to pop back to the first route: - -```dart -Navigator.of(context).popUntil((route) => route.isFirst); -``` - -Both of these will ensure the route is disposed of and the user is disconnected as a result. - -## Option 2: A Nested Navigator - -Another approach would be to introduce a new [Navigator](https://api.flutter.dev/flutter/widgets/Navigator-class.html). -This has the benefit that everything related to Stream chat is contained to a specific part of the widget tree. - -### Defining Routes and Subroutes - -In this example, our application has the following routes. - -```dart -const routeHome = '/'; -const routePrefixChat = '/chat/'; -const routeChatHome = '$routePrefixChat$routeChatChannels'; -const routeChatChannels = 'chat_channels'; -const routeChatChannel = 'chat_channel'; -``` - -For the `/chat/` subroutes (**routePrefixChat**), this approach initializes Stream Chat in our application and introduces a nested navigator. - -Let’s explore the code: - -```dart -void main() { - runApp(const MyApp()); -} - -class MyApp extends StatefulWidget { - const MyApp({ - Key? key, - }) : super(key: key); - - @override - State createState() => _MyAppState(); -} - -class _MyAppState extends State { - final GlobalKey navigatorKey = GlobalKey(); - - @override - Widget build(BuildContext context) { - return Provider.value( - value: navigatorKey, - child: MaterialApp( - navigatorKey: navigatorKey, - initialRoute: routeHome, - onGenerateRoute: (settings) { - late Widget page; - if (settings.name == routeHome) { - page = const HomeScreen(); - } else if (settings.name!.startsWith(routePrefixChat)) { - final subRoute = settings.name!.substring(routePrefixChat.length); - page = ChatSetup( - setupChatRoute: subRoute, - ); - } else { - throw Exception('Unknown route: ${settings.name}'); - } - - return MaterialPageRoute( - builder: (context) { - return page; - }, - settings: settings, - ); - }, - ), - ); - } -} -``` - -In the above code you’re: - -1. Creating a navigator key, passing it to `MaterialApp`, and exposing it to the whole application using Provider (you can expose it however you want). -2. Creating `onGenerateRoute` that specifies what page to show depending on the route. Most importantly, if the route contains the **routePrefixChat,** it navigates to the **ChatSetup** page and passes in the remainder of the route. - -For example, `Navigator.pushNamed(context, routeChatHome)` will navigate to the **ChatSetup** page and pass in the subroute **routeChatChannels.** - -### Stream Chat Initialization, User Connection, and Nested Navigation - -Within the **ChatSetup** widget, we’ll initialize Stream chat, connect a user, and create a new Navigator that handles the sub-navigation for the chat-specific pages. - -```dart -class ChatSetup extends StatefulWidget { - const ChatSetup({Key? key, required this.setupChatRoute}) : super(key: key); - - final String setupChatRoute; - - @override - State createState() => _ChatSetupState(); -} - -class _ChatSetupState extends State { - late final Future connectionFuture; - late final client = StreamChatClient( - 'KEY', - logLevel: Level.OFF, - ); - - @override - void initState() { - super.initState(); - connectionFuture = client.connectUser( - User(id: 'USER_ID'), - 'TOKEN', - ); - } - - @override - void dispose() { - client.disconnectUser(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Material( - child: StreamChat( - client: client, - child: FutureBuilder( - future: connectionFuture, - builder: (BuildContext context, AsyncSnapshot snapshot) { - switch (snapshot.connectionState) { - case ConnectionState.waiting: - return const Center( - child: CircularProgressIndicator(), - ); - default: - if (snapshot.hasError) { - return Text('Error: ${snapshot.error}'); - } else { - return Navigator( - initialRoute: widget.setupChatRoute, - onGenerateRoute: _onGenerateRoute, - ); - } - } - }, - ), - ), - ); - } - - Route _onGenerateRoute(RouteSettings settings) { - late Widget page; - switch (settings.name) { - case routeChatChannels: - page = ChannelListPage( - client: client, - ); - break; - case routeChatChannel: - final channel = settings.arguments as Channel; - page = StreamChannel( - channel: channel, - child: const ChannelPage(), - ); - break; - default: - throw Exception('Unknown route: ${settings.name}'); - } - return MaterialPageRoute( - builder: (context) { - return page; - }, - settings: settings, - ); - } -} -``` - -This **ChatSetup** widget is similar to what it was in the first option, the only difference is that we’re also introducing a nested navigator and handling those nested routes. - -The steps for the above code are: - -1. Create a **StreamChatClient** instance -2. Call `connectUser` within `initState` and await the result using a **FutureBuilder** -3. Introduce a **StreamChat** widget into the widget tree -4. Introduce a new **Navigator** for the chat-specific routes -5. Call `disconnectUser` within `dispose` - -### Displaying Stream Chat UI Widgets and Global Navigation - -Then finally, the **ChannelListPage** could look something like the following: - -```dart -class ChannelListPage extends StatefulWidget { - const ChannelListPage({ - super.key, - required this.client, - }); - - final StreamChatClient client; - - @override - State createState() => _ChannelListPageState(); -} - -class _ChannelListPageState extends State { - late final _controller = StreamChannelListController( - client: widget.client, - filter: Filter.in_( - 'members', - [StreamChat.of(context).currentUser!.id], - ), - sort: const [SortOption('last_message_at')], - ); - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - void _popChatPages() { - final nav = context.read>(); - nav.currentState!.pop(); - } - - @override - Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - _popChatPages(); - return true; - }, - child: Scaffold( - appBar: AppBar( - leading: BackButton( - onPressed: () { - _popChatPages(); - }, - ), - ), - body: RefreshIndicator( - onRefresh: _controller.refresh, - child: StreamChannelListView( - controller: _controller, - onChannelTap: (channel) { - Navigator.pushNamed(context, routeChatChannel, - arguments: channel); - }, - ), - ), - ), - ); - } -} -``` - -There are two important things to note in the above code: - -- We’re accessing the global **NavigatorState** using Provider and calling `pop()` on the back press. This will ensure that this entire route is disposed of and that the Stream connection is closed. -- Within `onChannelTap` we’re calling `pushNamed` and passing in the route to display a single channel page. This uses the navigator introduced in **ChatSetup**, which is the closest navigator within the widget tree. - -A few things to take note of: - -- You’ll always need to access the global navigator if you want to dispose of this route. you need to ensure that this route is popped or replaced, otherwise, the Stream connection will remain active for as long as the chat route is on the stack (or manually disconnected). - -## Option 3: Navigator 2.0 - Using GoRouter - -This final example will be a combination of the first two options. The following code is an example of how to initialize Stream Chat in a part of the widget tree using the [GoRouter package](https://pub.dev/packages/go_router). This solution will change depending on how you do routing in your Flutter application and which package (if any) you use. - -### Application Routes and Conditional Stream Initialization - -For this example we have the following routes and subroutes: - -```dart -|_ '/' -> home page - |_ 'settings/' - settings page - |_ 'chat/' - chat home, shows the channels list page - |_ 'channel/' - specific channel page -``` - -In the **MyApp** widget, we’ll initialize **GoRouter** and the **StreamChatClient**. -However, we will only expose the **StreamChat** widget for certain routes. -Additionally, we’ll create a **ChatSetup** widget that connects and disconnects the user. -This widget will also **only** be injected for the `/chat` routes. - -```dart -class MyApp extends StatefulWidget { - const MyApp({ - Key? key, - }) : super(key: key); - - @override - State createState() => _MyAppState(); -} - -class _MyAppState extends State { - final _client = StreamChatClient( - 'q29npdvqjr99', - logLevel: Level.OFF, - ); - bool wasPreviousRouteChat = false; - - late final _router = GoRouter( - initialLocation: '/', - navigatorBuilder: (context, state, child) { - if (state.location.startsWith('/chat')) { - wasPreviousRouteChat = true; - return StreamChat( - client: _client, - child: ChatSetup(client: _client, child: child), - ); - } else { - if (wasPreviousRouteChat) { - wasPreviousRouteChat = false; - return StreamChat( - client: _client, - child: child, - ); - } else { - return child; - } - } - }, - routes: [ - GoRoute( - path: '/', - builder: (context, state) => const HomeScreen(), - routes: [ - GoRoute( - path: 'settings', - builder: (context, state) => const SettingsPage(), - ), - GoRoute( - path: 'chat', - builder: (context, state) => const ChannelListPage(), - routes: [ - GoRoute( - path: 'channel', - builder: (context, state) { - final channel = state.extra as Channel; - return StreamChannel( - channel: channel, - child: const ChannelPage(), - ); - }, - ), - ], - ), - ], - ), - ], - ); - - @override - Widget build(BuildContext context) { - return MaterialApp.router( - routeInformationParser: _router.routeInformationParser, - routeInformationProvider: _router.routeInformationProvider, - routerDelegate: _router.routerDelegate, - ); - } -} -``` - -If you’re unfamiliar with GoRouter it will help to first read the documentation and then come back to this guide. - -There are a few important things to note in the above code: - -- Define our routes and subroutes -- Specify the initial route with `initialLocation` -- Use the `navigatorBuilder` to wrap certain routes with **StreamChat** and **ChatSetup**. - - The `wasPreviousRouteChat` \***\*boolean is used to determine if the previous route was a chat route. This is important because when you press the back button from the `/chat` route and navigate to the `/` route, the **StreamChat** widget still needs to be accessible while the navigation transition occurs. However, if you then navigate to the `/setting` route, you no longer need the **StreamChat\*\* widget and can safely remove it. - -### Navigating With GoRouter - -Within the **HomeScreen** you can create a button that on press navigates to the `/chat` route: - -```dart -GoRouter.of(context).go('/chat'); -``` - -### Connecting and Disconnecting Users - -Same as before, we’ll use the **ChatSetup** widget to connect and disconnect a user. -This time the widget also takes in a **child** widget to display once the connection is finished. -The child widget is dependent on the route we’re navigating to. - -```dart -class ChatSetup extends StatefulWidget { - const ChatSetup({ - Key? key, - required this.client, - required this.child, - }) : super(key: key); - - final StreamChatClient client; - final Widget child; - - @override - State createState() => _ChatSetupState(); -} - -class _ChatSetupState extends State { - late final Future connectionFuture; - - @override - void initState() { - super.initState(); - connectionFuture = widget.client.connectUser( - User(id: 'USER_ID'), - 'TOKEN', - ); - } - - @override - void dispose() { - widget.client.disconnectUser(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Material( - child: FutureBuilder( - future: connectionFuture, - builder: (BuildContext context, AsyncSnapshot snapshot) { - switch (snapshot.connectionState) { - case ConnectionState.waiting: - return const Center( - child: CircularProgressIndicator(), - ); - default: - if (snapshot.hasError) { - return Text('Error: ${snapshot.error}'); - } else { - return widget.child; - } - } - }, - ), - ); - } -} -``` - -This will connect the Stream chat user within `initState` and disconnect the user on `dispose`. This is similar to the previous examples we explored. - -### Displaying the Stream UI Widgets - -The **ChannelListPage** can look something like the following: - -```dart -class ChannelListPage extends StatefulWidget { - const ChannelListPage({ - super.key, - }); - - @override - State createState() => _ChannelListPageState(); -} - -class _ChannelListPageState extends State { - late final client = StreamChat.of(context).client; - late final _controller = StreamChannelListController( - client: client, - filter: Filter.in_( - 'members', - [StreamChat.of(context).currentUser!.id], - ), - sort: const [SortOption('last_message_at')], - ); - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(), - body: RefreshIndicator( - onRefresh: _controller.refresh, - child: StreamChannelListView( - controller: _controller, - onChannelTap: (channel) { - GoRouter.of(context).go('/chat/channel', extra: channel); - }, - ), - ), - ); - } -} -``` - -This is the same as before, the only difference is that the navigation is slightly different; now we’re navigating to the `/chat/channel` route for individual channel pages. - -## Conclusion - -This guide demonstrated three different ways to initialize Stream Chat in a part of the Flutter widget tree. - -There are two key takeaways - -1. Accessing **StreamChat** depends on your location in the widget tree -2. How you ultimately decide to expose **StreamChat** and connect users will be up to your application architecture and how you manage routing. - -The above are only examples that can be refined and tweaked to suit your needs. diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_user_list_controller.mdx b/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_user_list_controller.mdx index 885b1c82..0bee29ed 100644 --- a/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_user_list_controller.mdx +++ b/docusaurus/docs/Flutter/stream_chat_flutter_core/stream_user_list_controller.mdx @@ -27,7 +27,6 @@ class UserListPageState extends State { /// [StreamUserListController]. late final userListController = StreamUserListController( client: StreamChatCore.of(context).client, - filter: Filter.notEqual('id', StreamChatCore.of(context).currentUser!), ); ``` @@ -110,4 +109,4 @@ Widget build(BuildContext context) => Scaffold( ); ``` -In this case we're using the [`LazyLoadScrollView`](./lazy_load_scroll_view.mdx) widget to load more data when the user scrolls to the bottom of the list. +In this case we're using the [`LazyLoadScrollView`](./lazy_load_scroll_view.mdx) widget to load more data when the user scrolls to the bottom of the list. \ No newline at end of file diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_push_notifications.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_push_notifications.mdx index e845c3c6..328ba546 100644 --- a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_push_notifications.mdx +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_push_notifications.mdx @@ -128,7 +128,7 @@ If you're not sure if you've set up push notifications correctly (e.g. you don't 11. After configuring [stream-cli](https://github.com/GetStream/stream-cli) paste the following command on command line using your user ID ```shell -stream-cli chat test-push --user-id +stream chat:push:test -u ``` You should get a test push notification diff --git a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_push_notifications_v2.mdx b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_push_notifications_v2.mdx index dc9f5a32..285c2205 100644 --- a/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_push_notifications_v2.mdx +++ b/docusaurus/flutter_versioned_docs/version-3.x.x/Flutter/guides/adding_push_notifications_v2.mdx @@ -215,7 +215,7 @@ If you're not sure whether you've set up push notifications correctly, for examp 9. Tap on `Device ID` and copy it 11. After configuring [stream-cli](https://github.com/GetStream/stream-cli), run the following command using your user ID: ```shell -stream-cli chat test-push --user-id +stream chat:push:test -u ``` You should get a test push notification 🥳 diff --git a/melos.yaml b/melos.yaml index fbbfcb78..217f4bc5 100644 --- a/melos.yaml +++ b/melos.yaml @@ -1,9 +1,6 @@ name: stream_chat_flutter repository: https://github.com/GetStream/stream-chat-flutter -versioning: - mode: independent - packages: - packages/** @@ -97,10 +94,3 @@ scripts: npm install -g https://github.com/GetStream/stream-chat-docusaurus-cli && npx stream-chat-docusaurus -i -s description: Runs the docusaurus documentation locally. - -dev_dependencies: - dart_code_metrics: ^4.4.0 - -environment: - sdk: '>=2.17.0 <3.0.0' - flutter: '>=1.17.0 <3.0.0' \ No newline at end of file diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 86ce4e63..37e45f9e 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,35 @@ +## 5.0.0 + +- Included the changes from version [4.5.0](#450). + +🛑️ Breaking Changes from `5.0.0-beta.2` + +- `Channel.addMembers`, `Channel.removeMembers`, `Channel.inviteMembers` and `Channel.update` + positional params are now optional params. + + ```dart + // previous + channel.addMembers([...ids], message, hideHistory); + channel.removeMembers([...ids], message); + channel.inviteMembers([...ids], message); + channel.update({...channelData}, updateMessage); + + // new + channel.addMembers([...ids], message: ..., hideHistory: ...); + channel.removeMembers([...ids], message: ...); + channel.inviteMembers([...ids], message: ...); + channel.update({...channelData}, updateMessage: ...); + ``` + +## 5.0.0-beta.2 + +- Included the changes from version [4.4.0](#440) and [4.4.1](#441). + +## 5.0.0-beta.1 + +- Minor fixes. +- Removed deprecated code. + ## 4.6.0 ✅ Added diff --git a/packages/stream_chat/example/lib/main.dart b/packages/stream_chat/example/lib/main.dart index 8dde5c0d..07447264 100644 --- a/packages/stream_chat/example/lib/main.dart +++ b/packages/stream_chat/example/lib/main.dart @@ -4,7 +4,10 @@ import 'package:stream_chat/stream_chat.dart'; Future main() async { /// Create a new instance of [StreamChatClient] /// by passing the apikey obtained from your project dashboard. - final client = StreamChatClient('b67pax5b2wdq', logLevel: Level.INFO); + final client = StreamChatClient( + 'b67pax5b2wdq', + logLevel: Level.INFO, + ); /// Set the current user. In a production scenario, this should be done using /// a backend to generate a user token using our server SDK. diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 688ce834..d2d854a8 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -1064,9 +1064,9 @@ class Channel { /// See, https://getstream.io/chat/docs/other-rest/channel_update/?language=dart /// for more information. Future update( - Map channelData, [ + Map channelData, { Message? updateMessage, - ]) async { + }) async { _checkInitialized(); return _client.updateChannel( id!, @@ -1149,12 +1149,10 @@ class Channel { /// Add members to the channel. Future addMembers( - List memberIds, [ + List memberIds, { Message? message, - // TODO: Convert to optional parameters in v5.0.0 - // ignore: avoid_positional_boolean_parameters bool hideHistory = false, - ]) async { + }) async { _checkInitialized(); return _client.addChannelMembers( id!, @@ -1167,18 +1165,18 @@ class Channel { /// Invite members to the channel. Future inviteMembers( - List memberIds, [ + List memberIds, { Message? message, - ]) async { + }) async { _checkInitialized(); return _client.inviteChannelMembers(id!, type, memberIds, message: message); } /// Remove members from the channel. Future removeMembers( - List memberIds, [ + List memberIds, { Message? message, - ]) async { + }) async { _checkInitialized(); return _client.removeChannelMembers(id!, type, memberIds, message: message); } @@ -2086,10 +2084,6 @@ class ChannelClientState { (m) => m.user?.id == _channel.client.state.currentUser?.id, ); - /// User role for the current user. - @Deprecated('Please use currentUserChannelRole') - String? get currentUserRole => currentUserMember?.role; - /// Channel role for the current user String? get currentUserChannelRole => currentUserMember?.channelRole; diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 66dd285a..2a33bd35 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -70,15 +70,9 @@ class StreamChatClient { Duration receiveTimeout = const Duration(seconds: 6), StreamChatApi? chatApi, WebSocket? ws, - @Deprecated('Use [attachmentFileUploaderProvider] instead') - AttachmentFileUploader? attachmentFileUploader, - AttachmentFileUploaderProvider? attachmentFileUploaderProvider, - }) : assert( - attachmentFileUploader == null || - attachmentFileUploaderProvider == null, - 'You can only use one of [attachmentFileUploader] ' - 'or [attachmentFileUploaderProvider]', - ) { + AttachmentFileUploaderProvider attachmentFileUploaderProvider = + StreamAttachmentFileUploader.new, + }) { logger.info('Initiating new StreamChatClient'); final options = StreamHttpClientOptions( @@ -88,23 +82,13 @@ class StreamChatClient { headers: {'X-Stream-Client': defaultUserAgent}, ); - // TODO: simplify this once we remove the deprecated field. - final AttachmentFileUploaderProvider fileUploaderProvider; - if (attachmentFileUploaderProvider != null) { - fileUploaderProvider = attachmentFileUploaderProvider; - } else if (attachmentFileUploader != null) { - fileUploaderProvider = (httpClient) => attachmentFileUploader; - } else { - fileUploaderProvider = StreamAttachmentFileUploader.new; - } - _chatApi = chatApi ?? StreamChatApi( apiKey, options: options, tokenManager: _tokenManager, connectionIdManager: _connectionIdManager, - attachmentFileUploaderProvider: fileUploaderProvider, + attachmentFileUploaderProvider: attachmentFileUploaderProvider, logger: detachedLogger('🕸️'), ); diff --git a/packages/stream_chat/lib/src/client/key_stroke_handler.dart b/packages/stream_chat/lib/src/client/key_stroke_handler.dart index f67017b9..52878182 100644 --- a/packages/stream_chat/lib/src/client/key_stroke_handler.dart +++ b/packages/stream_chat/lib/src/client/key_stroke_handler.dart @@ -69,6 +69,7 @@ class KeyStrokeHandler { // This is needed to prevent the user from being stuck in typing mode. if (_lastTypingEvent != null) { // We don't need to handle the error here + // ignore: no-empty-block _stopTyping(_currentParentId).catchError((_) {}); } _cancelKeyStrokeTimer(); diff --git a/packages/stream_chat/lib/src/core/api/requests.dart b/packages/stream_chat/lib/src/core/api/requests.dart index fa83ff73..719b2b9b 100644 --- a/packages/stream_chat/lib/src/core/api/requests.dart +++ b/packages/stream_chat/lib/src/core/api/requests.dart @@ -62,8 +62,6 @@ class PaginationParams extends Equatable { /// ``` const PaginationParams({ this.limit = 10, - this.before = 10, - this.after = 10, this.offset, this.next, this.idAround, @@ -88,14 +86,6 @@ class PaginationParams extends Equatable { /// The amount of items requested from the APIs. final int limit; - /// The amount of items requested before message ID from the APIs. - @Deprecated('before is deprecated, use limit instead') - final int before; - - /// The amount of items requested after message ID from the APIs. - @Deprecated('after is deprecated, use limit instead') - final int after; - /// The offset of requesting items. final int? offset; @@ -165,8 +155,6 @@ class PaginationParams extends Equatable { }) => PaginationParams( limit: limit ?? this.limit, - before: before ?? this.before, - after: limit ?? this.after, offset: offset ?? this.offset, idAround: idAround ?? this.idAround, next: next ?? this.next, @@ -186,8 +174,6 @@ class PaginationParams extends Equatable { @override List get props => [ limit, - before, - after, offset, next, idAround, diff --git a/packages/stream_chat/lib/src/core/api/requests.g.dart b/packages/stream_chat/lib/src/core/api/requests.g.dart index 90bd789d..b70b8767 100644 --- a/packages/stream_chat/lib/src/core/api/requests.g.dart +++ b/packages/stream_chat/lib/src/core/api/requests.g.dart @@ -21,8 +21,6 @@ Map _$SortOptionToJson(SortOption instance) => PaginationParams _$PaginationParamsFromJson(Map json) => PaginationParams( limit: json['limit'] as int? ?? 10, - before: json['before'] as int? ?? 10, - after: json['after'] as int? ?? 10, offset: json['offset'] as int?, next: json['next'] as String?, idAround: json['id_around'] as String?, @@ -50,8 +48,6 @@ PaginationParams _$PaginationParamsFromJson(Map json) => Map _$PaginationParamsToJson(PaginationParams instance) { final val = { 'limit': instance.limit, - 'before': instance.before, - 'after': instance.after, }; void writeNotNull(String key, dynamic value) { diff --git a/packages/stream_chat/lib/src/core/api/stream_chat_api.dart b/packages/stream_chat/lib/src/core/api/stream_chat_api.dart index b3f29210..7e725204 100644 --- a/packages/stream_chat/lib/src/core/api/stream_chat_api.dart +++ b/packages/stream_chat/lib/src/core/api/stream_chat_api.dart @@ -23,10 +23,10 @@ class StreamChatApi { StreamHttpClientOptions? options, TokenManager? tokenManager, ConnectionIdManager? connectionIdManager, - AttachmentFileUploaderProvider? attachmentFileUploaderProvider, + AttachmentFileUploaderProvider attachmentFileUploaderProvider = + StreamAttachmentFileUploader.new, Logger? logger, - }) : _fileUploaderProvider = - attachmentFileUploaderProvider ?? StreamAttachmentFileUploader.new, + }) : _fileUploaderProvider = attachmentFileUploaderProvider, _client = client ?? StreamHttpClient( apiKey, diff --git a/packages/stream_chat/lib/src/core/models/attachment.dart b/packages/stream_chat/lib/src/core/models/attachment.dart index 72ff5525..4d9a3b30 100644 --- a/packages/stream_chat/lib/src/core/models/attachment.dart +++ b/packages/stream_chat/lib/src/core/models/attachment.dart @@ -49,7 +49,7 @@ class Attachment extends Equatable { if (file?.mimeType != null) 'mime_type': file?.mimeType?.mimeType, } { this.uploadState = uploadState ?? - ((assetUrl != null || imageUrl != null) + ((assetUrl != null || imageUrl != null || thumbUrl != null) ? const UploadState.success() : const UploadState.preparing()); } diff --git a/packages/stream_chat/lib/src/core/models/attachment_file.dart b/packages/stream_chat/lib/src/core/models/attachment_file.dart index edce0e1a..e6750843 100644 --- a/packages/stream_chat/lib/src/core/models/attachment_file.dart +++ b/packages/stream_chat/lib/src/core/models/attachment_file.dart @@ -47,7 +47,8 @@ class AttachmentFile { final String? _name; /// File name including its extension. - String? get name => _name ?? path?.split('/').last; + String? get name => + _name ?? path?.split(CurrentPlatform.isWindows ? r'\' : '/').last; /// Byte data for this file. Particularly useful if you want to manipulate /// its data or easily upload to somewhere else. @@ -85,6 +86,22 @@ class AttachmentFile { } return multiPartFile; } + + /// Creates a copy of this [AttachmentFile] but with the given fields + /// replaced with the new values. + AttachmentFile copyWith({ + String? path, + String? name, + Uint8List? bytes, + int? size, + }) { + return AttachmentFile( + path: path ?? this.path, + name: name ?? this.name, + bytes: bytes ?? this.bytes, + size: size ?? this.size, + ); + } } /// Union class to hold various [UploadState] of a attachment. diff --git a/packages/stream_chat/lib/src/core/models/channel_model.dart b/packages/stream_chat/lib/src/core/models/channel_model.dart index 942b9e63..8d01f89d 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.dart @@ -39,7 +39,6 @@ class ChannelModel { createdAt = createdAt ?? DateTime.now(), updatedAt = updatedAt ?? DateTime.now(), - // TODO: Make them top-level fields in v5 // For backwards compatibility, set 'disabled', 'hidden' // and 'truncated_at' in [extraData]. extraData = { diff --git a/packages/stream_chat/lib/src/core/models/member.dart b/packages/stream_chat/lib/src/core/models/member.dart index c2dc9eb1..8fe65b44 100644 --- a/packages/stream_chat/lib/src/core/models/member.dart +++ b/packages/stream_chat/lib/src/core/models/member.dart @@ -16,7 +16,6 @@ class Member extends Equatable { this.inviteAcceptedAt, this.inviteRejectedAt, this.invited = false, - this.role, this.channelRole, this.userId, this.isModerator = false, @@ -48,10 +47,6 @@ class Member extends Equatable { /// True if the user has been invited to the channel final bool invited; - /// The role of the user in the channel - @Deprecated('Please use channelRole') - final String? role; - /// The role of this member in the channel final String? channelRole; @@ -100,7 +95,6 @@ class Member extends Equatable { banned: banned ?? this.banned, banExpires: banExpires ?? this.banExpires, shadowBanned: shadowBanned ?? this.shadowBanned, - role: role ?? this.role, channelRole: channelRole ?? this.channelRole, userId: userId ?? this.userId, isModerator: isModerator ?? this.isModerator, @@ -117,7 +111,6 @@ class Member extends Equatable { inviteAcceptedAt, inviteRejectedAt, invited, - role, channelRole, userId, isModerator, diff --git a/packages/stream_chat/lib/src/core/models/member.g.dart b/packages/stream_chat/lib/src/core/models/member.g.dart index 1bcde74d..2cda32a4 100644 --- a/packages/stream_chat/lib/src/core/models/member.g.dart +++ b/packages/stream_chat/lib/src/core/models/member.g.dart @@ -17,7 +17,6 @@ Member _$MemberFromJson(Map json) => Member( ? null : DateTime.parse(json['invite_rejected_at'] as String), invited: json['invited'] as bool? ?? false, - role: json['role'] as String?, channelRole: json['channel_role'] as String?, userId: json['user_id'] as String?, isModerator: json['is_moderator'] as bool? ?? false, @@ -39,7 +38,6 @@ Map _$MemberToJson(Member instance) => { 'invite_accepted_at': instance.inviteAcceptedAt?.toIso8601String(), 'invite_rejected_at': instance.inviteRejectedAt?.toIso8601String(), 'invited': instance.invited, - 'role': instance.role, 'channel_role': instance.channelRole, 'user_id': instance.userId, 'is_moderator': instance.isModerator, diff --git a/packages/stream_chat/lib/src/core/models/user.dart b/packages/stream_chat/lib/src/core/models/user.dart index d97d009c..6cce01df 100644 --- a/packages/stream_chat/lib/src/core/models/user.dart +++ b/packages/stream_chat/lib/src/core/models/user.dart @@ -46,7 +46,6 @@ class User extends Equatable { this.language, }) : createdAt = createdAt ?? DateTime.now(), updatedAt = updatedAt ?? DateTime.now(), - // TODO: Make them top-level fields in v5 // For backwards compatibility, set 'name', 'image' in [extraData]. extraData = { ...extraData, diff --git a/packages/stream_chat/lib/src/core/platform_detector/platform_detector.dart b/packages/stream_chat/lib/src/core/platform_detector/platform_detector.dart index cf817cac..f3dcdbd2 100644 --- a/packages/stream_chat/lib/src/core/platform_detector/platform_detector.dart +++ b/packages/stream_chat/lib/src/core/platform_detector/platform_detector.dart @@ -4,25 +4,25 @@ import 'package:stream_chat/src/core/platform_detector/platform_detector_stub.da /// Possible platforms enum PlatformType { - /// + /// Android: android, - /// + /// iOS: ios, - /// + /// web: web, - /// + /// macOS: macOS, - /// + /// Windows: windows, - /// + /// Linux: linux, - /// + /// Fuchsia: fuchsia, } @@ -51,6 +51,9 @@ class CurrentPlatform { /// True if the app is running on fuchsia static bool get isFuchsia => type == PlatformType.fuchsia; + /// True if the app is running in test environment + static bool get isFlutterTest => isFlutterTestEnvironment; + /// Returns a string version of the platform static String get name { switch (type) { @@ -68,8 +71,6 @@ class CurrentPlatform { return 'linux'; case PlatformType.fuchsia: return 'fuchsia'; - default: - return ''; } } diff --git a/packages/stream_chat/lib/src/core/platform_detector/platform_detector_io.dart b/packages/stream_chat/lib/src/core/platform_detector/platform_detector_io.dart index da707eed..b9675d61 100644 --- a/packages/stream_chat/lib/src/core/platform_detector/platform_detector_io.dart +++ b/packages/stream_chat/lib/src/core/platform_detector/platform_detector_io.dart @@ -10,3 +10,8 @@ PlatformType get currentPlatform { if (Platform.isIOS) return PlatformType.ios; return PlatformType.android; } + +/// True if the app is running in test environment. +bool get isFlutterTestEnvironment { + return Platform.environment.containsKey('FLUTTER_TEST'); +} diff --git a/packages/stream_chat/lib/src/core/platform_detector/platform_detector_stub.dart b/packages/stream_chat/lib/src/core/platform_detector/platform_detector_stub.dart index 9d1a7f66..36d2eb33 100644 --- a/packages/stream_chat/lib/src/core/platform_detector/platform_detector_stub.dart +++ b/packages/stream_chat/lib/src/core/platform_detector/platform_detector_stub.dart @@ -1,6 +1,7 @@ import 'package:stream_chat/src/core/platform_detector/platform_detector.dart'; /// Stub implementation -PlatformType get currentPlatform { - throw UnimplementedError(); -} +PlatformType get currentPlatform => throw UnimplementedError(); + +/// Stub implementation +bool get isFlutterTestEnvironment => throw UnimplementedError(); diff --git a/packages/stream_chat/lib/src/core/platform_detector/platform_detector_web.dart b/packages/stream_chat/lib/src/core/platform_detector/platform_detector_web.dart index 324b4145..e1a08aa9 100644 --- a/packages/stream_chat/lib/src/core/platform_detector/platform_detector_web.dart +++ b/packages/stream_chat/lib/src/core/platform_detector/platform_detector_web.dart @@ -2,3 +2,8 @@ import 'package:stream_chat/src/core/platform_detector/platform_detector.dart'; /// Version running on web PlatformType get currentPlatform => PlatformType.web; + +/// True if the app is running in test environment. +/// +/// Always returns false as we don't have environment variables on web. +bool get isFlutterTestEnvironment => false; diff --git a/packages/stream_chat/lib/src/db/chat_persistence_client.dart b/packages/stream_chat/lib/src/db/chat_persistence_client.dart index b1769807..3dfbbc81 100644 --- a/packages/stream_chat/lib/src/db/chat_persistence_client.dart +++ b/packages/stream_chat/lib/src/db/chat_persistence_client.dart @@ -1,4 +1,5 @@ import 'package:stream_chat/src/core/api/requests.dart'; +import 'package:stream_chat/src/core/models/attachment_file.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; import 'package:stream_chat/src/core/models/channel_state.dart'; import 'package:stream_chat/src/core/models/event.dart'; @@ -8,6 +9,7 @@ import 'package:stream_chat/src/core/models/message.dart'; import 'package:stream_chat/src/core/models/reaction.dart'; import 'package:stream_chat/src/core/models/read.dart'; import 'package:stream_chat/src/core/models/user.dart'; +import 'package:stream_chat/src/core/platform_detector/platform_detector.dart'; import 'package:stream_chat/src/core/util/extension.dart'; /// A simple client used for persisting chat data locally. @@ -133,7 +135,7 @@ abstract class ChatPersistenceClient { /// Remove a pinned message by message [cids] Future deletePinnedMessageByCids(List cids); - /// Remove a channel by [cid] + /// Remove a channel by [channelId] Future deleteChannels(List cids); /// Updates the message data of a particular channel [cid] with @@ -243,7 +245,15 @@ abstract class ChatPersistenceClient { final cid = channel.cid; final reads = state.read; final members = state.members; - final messages = state.messages; + final Iterable? messages; + if (CurrentPlatform.isWeb) { + messages = state.messages?.where((it) => !it.attachments.any( + (attachment) => + attachment.uploadState != const UploadState.success(), + )); + } else { + messages = state.messages; + } final pinnedMessages = state.pinnedMessages; // Preparing deletion data @@ -255,7 +265,7 @@ abstract class ChatPersistenceClient { // preparing addition data channelWithReads[cid] = reads; channelWithMembers[cid] = members; - channelWithMessages[cid] = messages; + channelWithMessages[cid] = messages?.toList(); channelWithPinnedMessages[cid] = pinnedMessages; reactions.addAll(messages?.expand(_expandReactions) ?? []); diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 7f4c22ef..00b32399 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -5,12 +5,14 @@ export 'package:dio/src/cancel_token.dart'; export 'package:dio/src/dio_error.dart'; export 'package:dio/src/multipart_file.dart'; export 'package:dio/src/options.dart'; +export 'package:dio/src/options.dart' show ProgressCallback; export 'package:logging/logging.dart' show Logger, Level, LogRecord; export 'package:rate_limiter/rate_limiter.dart'; export 'package:uuid/uuid.dart'; export './src/core/api/attachment_file_uploader.dart'; export './src/core/api/requests.dart'; +export './src/core/api/requests.dart'; export './src/core/api/responses.dart'; export './src/core/api/stream_chat_api.dart' show PushProvider; export './src/core/error/error.dart'; @@ -41,6 +43,7 @@ export 'src/client/client.dart'; export 'src/client/key_stroke_handler.dart'; export 'src/core/api/attachment_file_uploader.dart' show AttachmentFileUploader; export 'src/core/api/requests.dart'; +export 'src/core/api/requests.dart'; export 'src/core/api/responses.dart'; export 'src/core/api/stream_chat_api.dart'; export 'src/core/error/error.dart'; @@ -61,6 +64,7 @@ export 'src/core/models/own_user.dart'; export 'src/core/models/reaction.dart'; export 'src/core/models/read.dart'; export 'src/core/models/user.dart'; +export 'src/core/platform_detector/platform_detector.dart'; export 'src/core/util/extension.dart'; export 'src/db/chat_persistence_client.dart'; export 'src/event_type.dart'; diff --git a/packages/stream_chat/lib/version.dart b/packages/stream_chat/lib/version.dart index 3acdad25..74e5ef10 100644 --- a/packages/stream_chat/lib/version.dart +++ b/packages/stream_chat/lib/version.dart @@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart'; /// Current package version /// Used in [StreamChatClient] to build the `x-stream-client` header // ignore: constant_identifier_names -const PACKAGE_VERSION = '4.6.0'; +const PACKAGE_VERSION = '5.0.0'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index 63c8b06c..304864b1 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat homepage: https://getstream.io/ description: The official Dart client for Stream Chat, a service for building chat applications. -version: 4.6.0 +version: 5.0.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues diff --git a/packages/stream_chat/test/fixtures/attachment_file.json b/packages/stream_chat/test/fixtures/attachment_file.json new file mode 100644 index 00000000..3735d15b --- /dev/null +++ b/packages/stream_chat/test/fixtures/attachment_file.json @@ -0,0 +1,5 @@ +{ + "size": 12, + "path": "/me/user/test.jpg", + "name": "test.jpg" +} \ No newline at end of file diff --git a/packages/stream_chat/test/src/client/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart index cf2fc042..c2f7490e 100644 --- a/packages/stream_chat/test/src/client/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel_test.dart @@ -108,7 +108,6 @@ void main() { }); }); - // TODO : test all persistence related logic in this group group('Initialized Channel with Persistence', () { late final client = MockStreamChatClientWithPersistence(); const channelId = 'test-channel-id'; @@ -1832,7 +1831,10 @@ void main() { ..message = updateMessage, ); - final res = await channel.update(channelData, updateMessage); + final res = await channel.update( + channelData, + updateMessage: updateMessage, + ); expect(res, isNotNull); expect(res.channel.cid, channelModel.cid); @@ -2028,7 +2030,7 @@ void main() { ..message = message, ); - final res = await channel.addMembers(memberIds, message); + final res = await channel.addMembers(memberIds, message: message); expect(res, isNotNull); expect(res.channel.cid, channelModel.cid); @@ -2060,7 +2062,7 @@ void main() { ..message = message, ); - final res = await channel.inviteMembers(memberIds, message); + final res = await channel.inviteMembers(memberIds, message: message); expect(res, isNotNull); expect(res.channel.cid, channelModel.cid); @@ -2093,7 +2095,7 @@ void main() { ..message = message, ); - final res = await channel.removeMembers(memberIds, message); + final res = await channel.removeMembers(memberIds, message: message); expect(res, isNotNull); expect(res.channel.cid, channelModel.cid); diff --git a/packages/stream_chat/test/src/client/retry_queue_test.dart b/packages/stream_chat/test/src/client/retry_queue_test.dart index a04feb3e..ddc5364e 100644 --- a/packages/stream_chat/test/src/client/retry_queue_test.dart +++ b/packages/stream_chat/test/src/client/retry_queue_test.dart @@ -68,6 +68,4 @@ void main() { expect(retryQueue.hasMessages, isTrue); }); }); - - // TODO: Add more tests once macbook is fixed :( } diff --git a/packages/stream_chat/test/src/core/models/attachment_file_test.dart b/packages/stream_chat/test/src/core/models/attachment_file_test.dart new file mode 100644 index 00000000..6dce83f9 --- /dev/null +++ b/packages/stream_chat/test/src/core/models/attachment_file_test.dart @@ -0,0 +1,39 @@ +import 'dart:typed_data'; + +import 'package:stream_chat/src/core/models/attachment_file.dart'; +import 'package:test/test.dart'; + +import '../../utils.dart'; + +void main() { + group('src/models/attachment_file', () { + test('should parse json correctly', () { + final attachment = + AttachmentFile.fromJson(jsonFixture('attachment_file.json')); + expect(attachment.name, 'test.jpg'); + expect(attachment.size, 12); + expect( + attachment.path, + '/me/user/test.jpg', + ); + }); + + test('should serialize to json correctly', () { + final attachment = AttachmentFile( + size: 12, + bytes: Uint8List.fromList([1, 2, 3]), + name: 'test.jpg', + path: '/me/user/test.jpg', + ); + + expect( + attachment.toJson(), + { + 'size': 12, + 'name': 'test.jpg', + 'path': '/me/user/test.jpg', + }, + ); + }); + }); +} diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index d7620978..27187060 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,8 +1,116 @@ +## 5.0.0 + +- Included the changes from version [4.5.0](#450). + +🐞 Fixed + +- [[#1326]](https://github.com/GetStream/stream-chat-flutter/issues/1326) Fixed hitting "enter" on + the android keyboard sends the message instead of going to a new line. + +✅ Added + +- Added `StreamMemberGridView` and `StreamMemberListView`. +- Added support for additional text field params in `StreamMessageInput` + * `maxLines` + * `minLines` + * `textInputAction` + * `keyboardType` + * `textCapitalization` +- Added `showStreamAttachmentPickerModalBottomSheet` to show the attachment picker modal bottom sheet. + +🔄 Changed + +- Removed Emoji picker from `StreamMessageInput`. + +## 5.0.0-beta.2 + +- Included the changes from version [4.4.0](#440) and [4.4.1](#441). + +🐞 Fixed + +- Fixed the unread message header in the message list view. +- Show dialog after clicking on the camera button and permission is denied. +- Fix Jiffy initialization. +- Fix loading to unread position in `StreamMessageListView`. +- Minor fixes and improvements. + +🔄 Changed + +- [[#1125]](https://github.com/GetStream/stream-chat-flutter/issues/1125) `defaultUserImage` + , `placeholderUserImage`, `reactionIcons`, and `enforceUniqueReactions` have been refactored out + of `StreamChatThemeData` and into the new `StreamChatConfigurationData` class. + +✅ Added + +- Added `StreamAutocomplete` widget for autocomplete triggers in `StreamMessageInput`. +- Added `StreamMessageInput.customAutocompleteTriggers` to allow users to define their custom + triggers. + +## 5.0.0-beta.1 + +- 🎉 Initial support for desktop 🖥️ and web 🧑‍💻 + - Right-click context menus for messages and full-screen attachments + - Upload and download attachments using the native desktop file system + - Press the "enter" key to send a message + - If you are quoting a message and have not yet typed any text, you can press the "esc" key to + remove the quoted message. + - A dedicated "X" button for removing a quoted message with your mouse + - Drag and drop attachment files to `StreamMessageInput` + - New `StreamMessageInput.draggingBorder` property to customize the border color of the + message input when dropping a file. + - Message reactions bubbles + - Hovering over a message reaction will show the users that have reacted to the message + - Desktop attachment sharing UI + - Selectable message text + - Gallery navigation controls with keyboard shortcuts (left and right arrow keys) + - Appropriate message sizing for large screens + - Right-click context menu for `StreamMessageListView` items + - `StreamMessageListView` items not swipeable on desktop & web + - Video support for Windows & Linux through `dart_vlc` + - Video support for macOS through `video_player_macos` + - Replace bottom sheets with dialogs where appropriate +- Other Additions ✅ + - `onQuotedMessageCleared` to `StreamMessageInput` + - `selected` and `selectedTileColor` to `StreamChannelListTile` + - `AttachmentUploadStateBuilder.inProgressBuilder` to `AttachmentUploadStateBuilder` + - `AttachmentUploadStateBuilder.successBuilder` to `AttachmentUploadStateBuilder` + - `AttachmentUploadStateBuilder.failedBuilder` to `AttachmentUploadStateBuilder` + - Translations: + - `couldNotReadBytesFromFileError` + - `downloadLabel` + - `toggleMuteUnmuteAction` + - `toggleMuteUnmuteGroupQuestion` + - `toggleMuteUnmuteGroupText` + - `toggleMuteUnmuteUserQuestion` + - `toggleMuteUnmuteUserText` + - Deprecated `showConfirmationDialog` in favor of `showConfirmationBottomSheet` + - Deprecated `showInfoDialog` in favor of `showInfoBottomSheet` + - Deprecated `wrapAttachmentWidget` in favor of the `WrapAttachmentWidget` class +- Breaking changes 🚧 + - `StreamImageAttachment.size` has been converted from type `Size` to type `BoxConstraints` + - `StreamFileAttachment.size` has been converted from type `Size` to type `BoxConstraints` + - `StreamGiphyAttachment.size` has been converted from type `Size` to type `BoxConstraints` + - `StreamVideoAttachment.size` has been converted from type `Size` to type `BoxConstraints` + - `StreamVideoThumbnailImage.width` and `StreamVideoThumbnailImage.height` have been removed in + favor of + `StreamVideoThumbnailImage.constraints` +- Dependency updates ⬆️ + - `chewie: ^1.3.0` -> `chewie: ^1.3.4` + - `path_provider: ^2.0.1` -> `path_provider: ^2.0.9` + - `video_player: ^2.1.0` -> `video_player: ^2.4.5` +- Code Improvements 🔧 + - Extracted many widgets to classes to improve readability, maintainability, and devtools usage. + - Organized internal directory structure + - Extracted typedefs to their own file + - Updated dartdoc documentation + - Various code readability improvements + ## 4.6.0 🐞 Fixed -- [[#1323]](https://github.com/GetStream/stream-chat-flutter/issues/1323): Fix message text hiding because of a [flutter bug](https://github.com/flutter/flutter/issues/110628). +- [[#1323]](https://github.com/GetStream/stream-chat-flutter/issues/1323): Fix message text hiding + because of a [flutter bug](https://github.com/flutter/flutter/issues/110628). ## 4.5.0 diff --git a/packages/stream_chat_flutter/README.md b/packages/stream_chat_flutter/README.md index 25310c5b..aedafb1d 100644 --- a/packages/stream_chat_flutter/README.md +++ b/packages/stream_chat_flutter/README.md @@ -60,6 +60,26 @@ We also use [video_player](https://pub.dev/packages/video_player) to reproduce v To pick images from the camera, we use the [image_picker](https://pub.dev/packages/image_picker) plugin. Follow [these instructions](https://pub.dev/packages/image_picker#ios) to check the requirements. +### Web + +For the web, edit your `index.html` and add the following in the `` tag in order to allow the SDK to override the right-click behaviour: + +```html + +``` + +### MacOS + +For MacOS use the [file_selector](https://pub.dev/packages/file_selector#macos) package. +Follow [these instructions](https://pub.dev/packages/file_selector#macos) to check the requirements. + +You also need to add the following [entitlement](https://docs.flutter.dev/development/platform-integration/desktop#entitlements-and-the-app-sandbox): + +```xml +com.apple.security.network.client + +``` + ### Troubleshooting It may happen that you have some problems building the app. diff --git a/packages/stream_chat_flutter/example/.gitignore b/packages/stream_chat_flutter/example/.gitignore index 9d532b18..72838985 100644 --- a/packages/stream_chat_flutter/example/.gitignore +++ b/packages/stream_chat_flutter/example/.gitignore @@ -32,7 +32,6 @@ /build/ # Web related -lib/generated_plugin_registrant.dart # Symbolication related app.*.symbols diff --git a/packages/stream_chat_flutter/example/android/app/build.gradle b/packages/stream_chat_flutter/example/android/app/build.gradle index 6576512a..26ee0824 100644 --- a/packages/stream_chat_flutter/example/android/app/build.gradle +++ b/packages/stream_chat_flutter/example/android/app/build.gradle @@ -40,7 +40,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.example" - minSdkVersion 22 + minSdkVersion 23 targetSdkVersion 31 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/packages/stream_chat_flutter/example/android/app/src/main/AndroidManifest.xml b/packages/stream_chat_flutter/example/android/app/src/main/AndroidManifest.xml index 5e2f5ec0..b286ebce 100644 --- a/packages/stream_chat_flutter/example/android/app/src/main/AndroidManifest.xml +++ b/packages/stream_chat_flutter/example/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,9 @@ additional functionality it is fine to subclass or reimplement FlutterApplication and put your custom class here. --> + + + main() async { WidgetsFlutterBinding.ensureInitialized(); /// Create a new instance of [StreamChatClient] passing the apikey obtained /// from your project dashboard. final client = StreamChatClient( 's2dxdhpxd94g', - logLevel: Level.INFO, + logLevel: Level.OFF, ); /// Set the current user and connect the websocket. In a production @@ -63,50 +66,272 @@ class MyApp extends StatelessWidget { final Channel channel; @override - Widget build(BuildContext context) => MaterialApp( - theme: ThemeData.light(), - darkTheme: ThemeData.dark(), - supportedLocales: const [ - Locale('en'), - Locale('hi'), - Locale('fr'), - Locale('it'), - Locale('es'), - ], - localizationsDelegates: GlobalStreamChatLocalizations.delegates, - builder: (context, widget) => StreamChat( - client: client, - child: widget, - ), - home: StreamChannel( - channel: channel, - child: const ChannelPage(), - ), - ); + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + theme: ThemeData.light(), + darkTheme: ThemeData.dark(), + // themeMode: ThemeMode.dark, + supportedLocales: const [ + Locale('en'), + Locale('hi'), + Locale('fr'), + Locale('it'), + Locale('es'), + ], + localizationsDelegates: GlobalStreamChatLocalizations.delegates, + builder: (context, widget) => StreamChat( + client: client, + child: widget, + ), + home: StreamChannel( + channel: channel, + child: const ResponsiveChat(), + ), + ); + } } -/// A list of messages sent in the current channel. -/// -/// This is implemented using [StreamMessageListView], -/// a widget that provides query -/// functionalities fetching the messages from the api and showing them in a -/// listView. -class ChannelPage extends StatelessWidget { - /// Creates the page that shows the list of messages - const ChannelPage({ +class ResponsiveChat extends StatelessWidget { + const ResponsiveChat({ super.key, }); @override - Widget build(BuildContext context) => Scaffold( - appBar: const StreamChannelHeader(), - body: Column( - children: const [ - Expanded( - child: StreamMessageListView(), + Widget build(BuildContext context) { + return ResponsiveBuilder( + builder: (context, sizingInformation) { + if (sizingInformation.isMobile) { + return ChannelListPage( + onTap: (c) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return StreamChannel( + channel: c, + child: ChannelPage( + onBackPressed: (context) { + Navigator.of( + context, + rootNavigator: true, + ).pop(); + }, + ), + ); + }, + ), + ); + }, + ); + } + + return const SplitView(); + }, + breakpoints: const ScreenBreakpoints( + desktop: 550, + tablet: 550, + watch: 300, + ), + ); + } +} + +class SplitView extends StatefulWidget { + const SplitView({ + super.key, + }); + + @override + _SplitViewState createState() => _SplitViewState(); +} + +class _SplitViewState extends State { + Channel? selectedChannel; + + @override + Widget build(BuildContext context) { + return Flex( + direction: Axis.horizontal, + children: [ + Flexible( + child: ChannelListPage( + onTap: (channel) { + setState(() { + selectedChannel = channel; + }); + }, + selectedChannel: selectedChannel, + ), + ), + Flexible( + flex: 2, + child: ClipPath( + child: Scaffold( + body: selectedChannel != null + ? StreamChannel( + key: ValueKey(selectedChannel!.cid), + channel: selectedChannel!, + child: const ChannelPage(showBackButton: false), + ) + : Center( + child: Text( + 'Pick a channel to show the messages 💬', + style: Theme.of(context).textTheme.headline5, + ), + ), ), - StreamMessageInput(attachmentLimit: 3), - ], + ), + ), + ], + ); + } +} + +class ChannelListPage extends StatefulWidget { + const ChannelListPage({ + super.key, + this.onTap, + this.selectedChannel, + }); + + final void Function(Channel)? onTap; + final Channel? selectedChannel; + + @override + State createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + late final _listController = StreamChannelListController( + client: StreamChat.of(context).client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + limit: 20, + ); + + @override + Widget build(BuildContext context) => Scaffold( + body: StreamChannelListView( + onChannelTap: widget.onTap, + controller: _listController, + itemBuilder: (context, channels, index, defaultWidget) { + return defaultWidget.copyWith( + selected: channels[index] == widget.selectedChannel, + ); + }, ), ); } + +class ChannelPage extends StatefulWidget { + const ChannelPage({ + super.key, + this.showBackButton = true, + this.onBackPressed, + }); + + final bool showBackButton; + final void Function(BuildContext)? onBackPressed; + + @override + State createState() => _ChannelPageState(); +} + +class _ChannelPageState extends State { + late final messageInputController = StreamMessageInputController(); + final focusNode = FocusNode(); + + @override + Widget build(BuildContext context) => Navigator( + onGenerateRoute: (settings) => MaterialPageRoute( + builder: (context) => Scaffold( + appBar: StreamChannelHeader( + onBackPressed: widget.onBackPressed != null + ? () { + widget.onBackPressed!(context); + } + : null, + showBackButton: widget.showBackButton, + ), + body: Column( + children: [ + Expanded( + child: StreamMessageListView( + onMessageSwiped: + (CurrentPlatform.isAndroid || CurrentPlatform.isIos) + ? reply + : null, + threadBuilder: (context, parent) { + return ThreadPage( + parent: parent!, + ); + }, + messageBuilder: + (context, details, messages, defaultWidget) { + return defaultWidget.copyWith( + onReplyTap: reply, + ); + }, + ), + ), + StreamMessageInput( + onQuotedMessageCleared: + messageInputController.clearQuotedMessage, + focusNode: focusNode, + messageInputController: messageInputController, + ), + ], + ), + ), + ), + ); + + void reply(Message message) { + messageInputController.quotedMessage = message; + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + focusNode.requestFocus(); + }); + } + + @override + void dispose() { + focusNode.dispose(); + super.dispose(); + } +} + +class ThreadPage extends StatelessWidget { + const ThreadPage({ + super.key, + required this.parent, + }); + + final Message parent; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: StreamThreadHeader( + parent: parent, + ), + body: Column( + children: [ + Expanded( + child: StreamMessageListView( + parentMessage: parent, + ), + ), + StreamMessageInput( + messageInputController: StreamMessageInputController( + message: Message(parentId: parent.id), + ), + ), + ], + ), + ); + } +} diff --git a/packages/stream_chat_flutter/example/lib/split_view.dart b/packages/stream_chat_flutter/example/lib/split_view.dart index 5b9cc6fb..4ecc6079 100644 --- a/packages/stream_chat_flutter/example/lib/split_view.dart +++ b/packages/stream_chat_flutter/example/lib/split_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -void main() async { +Future main() async { final client = StreamChatClient( 's2dxdhpxd94g', logLevel: Level.INFO, @@ -29,13 +29,15 @@ class MyApp extends StatelessWidget { final StreamChatClient client; @override - Widget build(BuildContext context) => MaterialApp( - builder: (context, child) => StreamChat( - client: client, - child: child, - ), - home: const SplitView(), - ); + Widget build(BuildContext context) { + return MaterialApp( + builder: (context, child) => StreamChat( + client: client, + child: child, + ), + home: const SplitView(), + ); + } } class SplitView extends StatefulWidget { @@ -51,37 +53,39 @@ class _SplitViewState extends State { Channel? selectedChannel; @override - Widget build(BuildContext context) => Flex( - direction: Axis.horizontal, - children: [ - Flexible( - child: ChannelListPage( - onTap: (channel) { - setState(() { - selectedChannel = channel; - }); - }, - ), + Widget build(BuildContext context) { + return Flex( + direction: Axis.horizontal, + children: [ + Flexible( + child: ChannelListPage( + onTap: (channel) { + setState(() { + selectedChannel = channel; + }); + }, ), - Flexible( - flex: 2, - child: Scaffold( - body: selectedChannel != null - ? StreamChannel( - key: ValueKey(selectedChannel!.cid), - channel: selectedChannel!, - child: const ChannelPage(), - ) - : Center( - child: Text( - 'Pick a channel to show the messages 💬', - style: Theme.of(context).textTheme.headline5, - ), + ), + Flexible( + flex: 2, + child: Scaffold( + body: selectedChannel != null + ? StreamChannel( + key: ValueKey(selectedChannel!.cid), + channel: selectedChannel!, + child: const ChannelPage(), + ) + : Center( + child: Text( + 'Pick a channel to show the messages 💬', + style: Theme.of(context).textTheme.headline5, ), - ), + ), ), - ], - ); + ), + ], + ); + } } class ChannelListPage extends StatefulWidget { diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart index 2b388d6a..b25311d5 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart @@ -1,5 +1,4 @@ // ignore_for_file: public_member_api_docs -// ignore_for_file: prefer_expression_function_bodies import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -32,7 +31,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// and [StreamMessageInput] /// /// If you now run the simulator you will see a single channel UI. -void main() async { +Future main() async { final client = StreamChatClient( 's2dxdhpxd94g', logLevel: Level.INFO, @@ -90,15 +89,18 @@ class ChannelPage extends StatelessWidget { }); @override - Widget build(BuildContext context) => Scaffold( - appBar: const StreamChannelHeader(), - body: Column( - children: const [ - Expanded( - child: StreamMessageListView(), - ), - StreamMessageInput(), - ], - ), - ); + // ignore: prefer_expression_function_bodies + Widget build(BuildContext context) { + return Scaffold( + appBar: const StreamChannelHeader(), + body: Column( + children: const [ + Expanded( + child: StreamMessageListView(), + ), + StreamMessageInput(), + ], + ), + ); + } } diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart index 6992527f..7801e9f1 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart @@ -1,6 +1,4 @@ // ignore_for_file: public_member_api_docs -// ignore_for_file: prefer_expression_function_bodies - import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -31,7 +29,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// [StreamChannelListView] handles pagination /// and updates automatically when new channels are created or when a new /// message is added to a channel. -void main() async { +Future main() async { final client = StreamChatClient( 's2dxdhpxd94g', logLevel: Level.INFO, @@ -100,23 +98,25 @@ class _ChannelListPageState extends State { } @override - Widget build(BuildContext context) => Scaffold( - body: RefreshIndicator( - onRefresh: _controller.refresh, - child: StreamChannelListView( - controller: _controller, - onChannelTap: (channel) => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => StreamChannel( - channel: channel, - child: const ChannelPage(), - ), + Widget build(BuildContext context) { + return Scaffold( + body: RefreshIndicator( + onRefresh: _controller.refresh, + child: StreamChannelListView( + controller: _controller, + onChannelTap: (channel) => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => StreamChannel( + channel: channel, + child: const ChannelPage(), ), ), ), ), - ); + ), + ); + } } class ChannelPage extends StatelessWidget { @@ -125,15 +125,17 @@ class ChannelPage extends StatelessWidget { }); @override - Widget build(BuildContext context) => Scaffold( - appBar: const StreamChannelHeader(), - body: Column( - children: const [ - Expanded( - child: StreamMessageListView(), - ), - StreamMessageInput(), - ], - ), - ); + Widget build(BuildContext context) { + return Scaffold( + appBar: const StreamChannelHeader(), + body: Column( + children: const [ + Expanded( + child: StreamMessageListView(), + ), + StreamMessageInput(), + ], + ), + ); + } } diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart index fbadcf0a..110f8d01 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart @@ -1,5 +1,4 @@ // ignore_for_file: public_member_api_docs -// ignore_for_file: prefer_expression_function_bodies import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart index e4886df3..439c4718 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart @@ -1,6 +1,4 @@ // ignore_for_file: public_member_api_docs -// ignore_for_file: prefer_expression_function_bodies - import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -106,21 +104,23 @@ class ChannelPage extends StatelessWidget { }); @override - Widget build(BuildContext context) => Scaffold( - appBar: const StreamChannelHeader(), - body: Column( - children: [ - Expanded( - child: StreamMessageListView( - threadBuilder: (_, parentMessage) => ThreadPage( - parent: parentMessage, - ), + Widget build(BuildContext context) { + return Scaffold( + appBar: const StreamChannelHeader(), + body: Column( + children: [ + Expanded( + child: StreamMessageListView( + threadBuilder: (_, parentMessage) => ThreadPage( + parent: parentMessage, ), ), - const StreamMessageInput(), - ], - ), - ); + ), + const StreamMessageInput(), + ], + ), + ); + } } class ThreadPage extends StatelessWidget { diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart index ae11a8a0..3a8ff420 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart @@ -1,5 +1,4 @@ // ignore_for_file: public_member_api_docs -// ignore_for_file: prefer_expression_function_bodies import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -86,21 +85,23 @@ class _ChannelListPageState extends State { } @override - Widget build(BuildContext context) => Scaffold( - body: StreamChannelListView( - controller: _listController, - onChannelTap: (channel) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => StreamChannel( - channel: channel, - child: const ChannelPage(), - ), + Widget build(BuildContext context) { + return Scaffold( + body: StreamChannelListView( + controller: _listController, + onChannelTap: (channel) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: const ChannelPage(), ), - ); - }, - ), - ); + ), + ); + }, + ), + ); + } } class ChannelPage extends StatelessWidget { diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart index 1b416e91..7dbe41bf 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart @@ -1,6 +1,4 @@ // ignore_for_file: public_member_api_docs -// ignore_for_file: prefer_expression_function_bodies - import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -126,21 +124,23 @@ class _ChannelListPageState extends State { } @override - Widget build(BuildContext context) => Scaffold( - body: StreamChannelListView( - controller: _listController, - onChannelTap: (channel) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => StreamChannel( - channel: channel, - child: const ChannelPage(), - ), + Widget build(BuildContext context) { + return Scaffold( + body: StreamChannelListView( + controller: _listController, + onChannelTap: (channel) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: const ChannelPage(), ), - ); - }, - ), - ); + ), + ); + }, + ), + ); + } } class ChannelPage extends StatelessWidget { @@ -149,21 +149,23 @@ class ChannelPage extends StatelessWidget { }); @override - Widget build(BuildContext context) => Scaffold( - appBar: const StreamChannelHeader(), - body: Column( - children: [ - Expanded( - child: StreamMessageListView( - threadBuilder: (_, parentMessage) => ThreadPage( - parent: parentMessage, - ), + Widget build(BuildContext context) { + return Scaffold( + appBar: const StreamChannelHeader(), + body: Column( + children: [ + Expanded( + child: StreamMessageListView( + threadBuilder: (_, parentMessage) => ThreadPage( + parent: parentMessage, ), ), - const StreamMessageInput(), - ], - ), - ); + ), + const StreamMessageInput(), + ], + ), + ); + } } class ThreadPage extends StatelessWidget { diff --git a/packages/stream_chat_flutter/example/linux/.gitignore b/packages/stream_chat_flutter/example/linux/.gitignore new file mode 100644 index 00000000..d3896c98 --- /dev/null +++ b/packages/stream_chat_flutter/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/packages/stream_chat_flutter/example/linux/CMakeLists.txt b/packages/stream_chat_flutter/example/linux/CMakeLists.txt new file mode 100644 index 00000000..a558bc45 --- /dev/null +++ b/packages/stream_chat_flutter/example/linux/CMakeLists.txt @@ -0,0 +1,116 @@ +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +set(BINARY_NAME "example") +set(APPLICATION_ID "com.example.example") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Configure build options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Application build +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) +apply_standard_settings(${BINARY_NAME}) +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) +add_dependencies(${BINARY_NAME} flutter_assemble) +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/packages/stream_chat_flutter/example/linux/flutter/CMakeLists.txt b/packages/stream_chat_flutter/example/linux/flutter/CMakeLists.txt new file mode 100644 index 00000000..33fd5801 --- /dev/null +++ b/packages/stream_chat_flutter/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,87 @@ +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/packages/stream_chat_flutter/example/linux/flutter/generated_plugin_registrant.cc b/packages/stream_chat_flutter/example/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..bb19d0b5 --- /dev/null +++ b/packages/stream_chat_flutter/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,35 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) dart_vlc_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DartVlcPlugin"); + dart_vlc_plugin_register_with_registrar(dart_vlc_registrar); + g_autoptr(FlPluginRegistrar) desktop_drop_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin"); + desktop_drop_plugin_register_with_registrar(desktop_drop_registrar); + g_autoptr(FlPluginRegistrar) screen_retriever_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); + screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); + g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); + sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) window_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); + window_manager_plugin_register_with_registrar(window_manager_registrar); +} diff --git a/packages/stream_chat_flutter/example/linux/flutter/generated_plugin_registrant.h b/packages/stream_chat_flutter/example/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..e0f0a47b --- /dev/null +++ b/packages/stream_chat_flutter/example/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/stream_chat_flutter/example/linux/flutter/generated_plugins.cmake b/packages/stream_chat_flutter/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 00000000..fb9923e9 --- /dev/null +++ b/packages/stream_chat_flutter/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,29 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + dart_vlc + desktop_drop + screen_retriever + sqlite3_flutter_libs + url_launcher_linux + window_manager +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/stream_chat_flutter/example/linux/main.cc b/packages/stream_chat_flutter/example/linux/main.cc new file mode 100644 index 00000000..e7c5c543 --- /dev/null +++ b/packages/stream_chat_flutter/example/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/packages/stream_chat_flutter/example/linux/my_application.cc b/packages/stream_chat_flutter/example/linux/my_application.cc new file mode 100644 index 00000000..0ba8f430 --- /dev/null +++ b/packages/stream_chat_flutter/example/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/packages/stream_chat_flutter/example/linux/my_application.h b/packages/stream_chat_flutter/example/linux/my_application.h new file mode 100644 index 00000000..72271d5e --- /dev/null +++ b/packages/stream_chat_flutter/example/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/packages/stream_chat_flutter/example/macos/Runner/DebugProfile.entitlements b/packages/stream_chat_flutter/example/macos/Runner/DebugProfile.entitlements index e585d0e0..b8a3263f 100644 --- a/packages/stream_chat_flutter/example/macos/Runner/DebugProfile.entitlements +++ b/packages/stream_chat_flutter/example/macos/Runner/DebugProfile.entitlements @@ -10,5 +10,9 @@ com.apple.security.network.client + com.apple.security.files.user-selected.read-only + + com.apple.security.files.user-selected.read-write + diff --git a/packages/stream_chat_flutter/example/macos/Runner/Info.plist b/packages/stream_chat_flutter/example/macos/Runner/Info.plist index 4789daa6..891f6b15 100644 --- a/packages/stream_chat_flutter/example/macos/Runner/Info.plist +++ b/packages/stream_chat_flutter/example/macos/Runner/Info.plist @@ -28,5 +28,7 @@ MainMenu NSPrincipalClass NSApplication + NSPhotoLibraryUsageDescription + In order to access your photo library diff --git a/packages/stream_chat_flutter/example/macos/Runner/MainFlutterWindow.swift b/packages/stream_chat_flutter/example/macos/Runner/MainFlutterWindow.swift index 2722837e..0ebf1c76 100644 --- a/packages/stream_chat_flutter/example/macos/Runner/MainFlutterWindow.swift +++ b/packages/stream_chat_flutter/example/macos/Runner/MainFlutterWindow.swift @@ -4,12 +4,12 @@ import FlutterMacOS class MainFlutterWindow: NSWindow { override func awakeFromNib() { let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame self.contentViewController = flutterViewController self.setFrame(windowFrame, display: true) RegisterGeneratedPlugins(registry: flutterViewController) - super.awakeFromNib() } -} +} \ No newline at end of file diff --git a/packages/stream_chat_flutter/example/pubspec.yaml b/packages/stream_chat_flutter/example/pubspec.yaml index 12a7dc63..c9fc3f0a 100644 --- a/packages/stream_chat_flutter/example/pubspec.yaml +++ b/packages/stream_chat_flutter/example/pubspec.yaml @@ -24,9 +24,10 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. collection: ^1.15.0 - cupertino_icons: ^1.0.3 + cupertino_icons: ^1.0.4 flutter: sdk: flutter + responsive_builder: ^0.4.2 stream_chat_flutter: path: ../ stream_chat_localizations: diff --git a/packages/stream_chat_flutter/example/web/icons/Icon-maskable-192.png b/packages/stream_chat_flutter/example/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000000000000000000000000000000000000..eb9b4d76e525556d5d89141648c724331630325d GIT binary patch literal 5594 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 X>c!)3PgKfL&lX^$Z>Cpu&6)6jvi^Z! literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/example/web/icons/Icon-maskable-512.png b/packages/stream_chat_flutter/example/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..d69c56691fbdb0b7efa65097c7cc1edac12a6d3e GIT binary patch literal 20998 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 MPgg&ebxsLQ025IeI{*Lx literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/example/web/index.html b/packages/stream_chat_flutter/example/web/index.html index fb053565..9131c18e 100644 --- a/packages/stream_chat_flutter/example/web/index.html +++ b/packages/stream_chat_flutter/example/web/index.html @@ -11,7 +11,6 @@ Fore more details: * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base --> - @@ -31,7 +30,7 @@ example - + diff --git a/packages/stream_chat_flutter/example/windows/.gitignore b/packages/stream_chat_flutter/example/windows/.gitignore new file mode 100644 index 00000000..d492d0d9 --- /dev/null +++ b/packages/stream_chat_flutter/example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/packages/stream_chat_flutter/example/windows/CMakeLists.txt b/packages/stream_chat_flutter/example/windows/CMakeLists.txt new file mode 100644 index 00000000..1633297a --- /dev/null +++ b/packages/stream_chat_flutter/example/windows/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.14) +project(example LANGUAGES CXX) + +set(BINARY_NAME "example") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Configure build options. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() + +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/packages/stream_chat_flutter/example/windows/flutter/CMakeLists.txt b/packages/stream_chat_flutter/example/windows/flutter/CMakeLists.txt new file mode 100644 index 00000000..b2e4bd8d --- /dev/null +++ b/packages/stream_chat_flutter/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,103 @@ +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/packages/stream_chat_flutter/example/windows/flutter/generated_plugin_registrant.cc b/packages/stream_chat_flutter/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..c01a56ac --- /dev/null +++ b/packages/stream_chat_flutter/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,41 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + ConnectivityPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); + DartVlcPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DartVlcPlugin")); + DesktopDropPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DesktopDropPlugin")); + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); + FlutterNativeViewPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterNativeViewPlugin")); + ScreenRetrieverPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); + Sqlite3FlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); + ThumblrWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ThumblrWindowsPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WindowManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowManagerPlugin")); +} diff --git a/packages/stream_chat_flutter/example/windows/flutter/generated_plugin_registrant.h b/packages/stream_chat_flutter/example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..dc139d85 --- /dev/null +++ b/packages/stream_chat_flutter/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/stream_chat_flutter/example/windows/flutter/generated_plugins.cmake b/packages/stream_chat_flutter/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 00000000..72783874 --- /dev/null +++ b/packages/stream_chat_flutter/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,33 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + connectivity_plus_windows + dart_vlc + desktop_drop + file_selector_windows + flutter_native_view + screen_retriever + sqlite3_flutter_libs + thumblr_windows + url_launcher_windows + window_manager +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/stream_chat_flutter/example/windows/runner/CMakeLists.txt b/packages/stream_chat_flutter/example/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..de2d8916 --- /dev/null +++ b/packages/stream_chat_flutter/example/windows/runner/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) +apply_standard_settings(${BINARY_NAME}) +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/packages/stream_chat_flutter/example/windows/runner/Runner.rc b/packages/stream_chat_flutter/example/windows/runner/Runner.rc new file mode 100644 index 00000000..5fdea291 --- /dev/null +++ b/packages/stream_chat_flutter/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#ifdef FLUTTER_BUILD_NUMBER +#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#else +#define VERSION_AS_NUMBER 1,0,0 +#endif + +#ifdef FLUTTER_BUILD_NAME +#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "example.exe" "\0" + VALUE "ProductName", "example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/packages/stream_chat_flutter/example/windows/runner/flutter_window.cpp b/packages/stream_chat_flutter/example/windows/runner/flutter_window.cpp new file mode 100644 index 00000000..b43b9095 --- /dev/null +++ b/packages/stream_chat_flutter/example/windows/runner/flutter_window.cpp @@ -0,0 +1,61 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/packages/stream_chat_flutter/example/windows/runner/flutter_window.h b/packages/stream_chat_flutter/example/windows/runner/flutter_window.h new file mode 100644 index 00000000..6da0652f --- /dev/null +++ b/packages/stream_chat_flutter/example/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/packages/stream_chat_flutter/example/windows/runner/main.cpp b/packages/stream_chat_flutter/example/windows/runner/main.cpp new file mode 100644 index 00000000..bcb57b0e --- /dev/null +++ b/packages/stream_chat_flutter/example/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/packages/stream_chat_flutter/example/windows/runner/resource.h b/packages/stream_chat_flutter/example/windows/runner/resource.h new file mode 100644 index 00000000..66a65d1e --- /dev/null +++ b/packages/stream_chat_flutter/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/packages/stream_chat_flutter/example/windows/runner/resources/app_icon.ico b/packages/stream_chat_flutter/example/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c04e20caf6370ebb9253ad831cc31de4a9c965f6 GIT binary patch literal 33772 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 literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/example/windows/runner/runner.exe.manifest b/packages/stream_chat_flutter/example/windows/runner/runner.exe.manifest new file mode 100644 index 00000000..c977c4a4 --- /dev/null +++ b/packages/stream_chat_flutter/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/packages/stream_chat_flutter/example/windows/runner/utils.cpp b/packages/stream_chat_flutter/example/windows/runner/utils.cpp new file mode 100644 index 00000000..d19bdbbc --- /dev/null +++ b/packages/stream_chat_flutter/example/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + if (target_length == 0) { + return std::string(); + } + std::string utf8_string; + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/packages/stream_chat_flutter/example/windows/runner/utils.h b/packages/stream_chat_flutter/example/windows/runner/utils.h new file mode 100644 index 00000000..3879d547 --- /dev/null +++ b/packages/stream_chat_flutter/example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/stream_chat_flutter/example/windows/runner/win32_window.cpp b/packages/stream_chat_flutter/example/windows/runner/win32_window.cpp new file mode 100644 index 00000000..c10f08dc --- /dev/null +++ b/packages/stream_chat_flutter/example/windows/runner/win32_window.cpp @@ -0,0 +1,245 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/packages/stream_chat_flutter/example/windows/runner/win32_window.h b/packages/stream_chat_flutter/example/windows/runner/win32_window.h new file mode 100644 index 00000000..17ba4311 --- /dev/null +++ b/packages/stream_chat_flutter/example/windows/runner/win32_window.h @@ -0,0 +1,98 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/packages/stream_chat_flutter/lib/conditional_parent_builder/README.md b/packages/stream_chat_flutter/lib/conditional_parent_builder/README.md new file mode 100644 index 00000000..f4cd55e3 --- /dev/null +++ b/packages/stream_chat_flutter/lib/conditional_parent_builder/README.md @@ -0,0 +1,25 @@ +# conditional_parent_builder + +A widget that allows developers to conditionally wrap a child widget with a parent widget. +This is useful in situations where a child widget should always be built, but should be wrapped +by another widget if certain conditions are met. + +In the following real-world example, we conditionally wrap the child widget +tree with a context menu if the message is not deleted: +```dart +ConditionalParentBuilder( + builder: (context, child) { + if (!widget.message.isDeleted) { + return ContextMenuArea( + builder: (context) => buildContextMenu(), + child: child, + ); + } else { + return child; + } + }, + child: Material(...), +), +``` +This example can be found in the `stream_chat_flutter` source code under +`src/message_widget/message_widget.dart`. \ No newline at end of file diff --git a/packages/stream_chat_flutter/lib/conditional_parent_builder/conditional_parent_builder.dart b/packages/stream_chat_flutter/lib/conditional_parent_builder/conditional_parent_builder.dart new file mode 100644 index 00000000..8314757d --- /dev/null +++ b/packages/stream_chat_flutter/lib/conditional_parent_builder/conditional_parent_builder.dart @@ -0,0 +1,55 @@ +library conditional_parent_builder; + +import 'package:flutter/material.dart'; + +/// {@template parentBuilder} +/// A function that provides the [BuildContext] and the [child] widget. +/// {@endtemplate} +typedef ParentBuilder = Widget Function( + BuildContext context, + Widget child, +); + +/// {@template conditionalParentBuilder} +/// A widget that allows developers to conditionally wrap the [child] widget +/// with a parent widget. +/// +/// In the following real-world example, we conditionally wrap the child widget +/// tree with a context menu if the message is not deleted: +/// ```dart +/// ConditionalParentBuilder( +/// builder: (context, child) { +/// if (!widget.message.isDeleted) { +/// return ContextMenuArea( +/// builder: (context) => buildContextMenu(), +/// child: child, +/// ); +/// } else { +/// return child; +/// } +/// }, +/// child: Material(...), +/// ), +/// ``` +/// This example can be found in the `stream_chat_flutter` source code under +/// `src/message_widget/message_widget.dart`. +/// {@endtemplate} +class ConditionalParentBuilder extends StatelessWidget { + /// {@macro conditionalParentBuilder} + const ConditionalParentBuilder({ + super.key, + required this.builder, + required this.child, + }); + + /// {@macro parentBuilder} + final ParentBuilder builder; + + /// The child widget to build. + final Widget child; + + @override + Widget build(BuildContext context) { + return builder.call(context, child); + } +} diff --git a/packages/stream_chat_flutter/lib/platform_widget_builder/README.md b/packages/stream_chat_flutter/lib/platform_widget_builder/README.md new file mode 100644 index 00000000..c936a552 --- /dev/null +++ b/packages/stream_chat_flutter/lib/platform_widget_builder/README.md @@ -0,0 +1,28 @@ +# platform_widget_builder + +This package is a more specialized version of the [flutter_platform_widgets](https://pub.dev/packages/flutter_platform_widgets) package. +It provides two specialized widget builders: +* `PlatformWidgetBuilder` +* `DesktopWidgetBuilder` + +### `PlatformWidgetBuilder` +This widget differs from the `PlatformWidgetBuilder` found in the `flutter_platform_widgets` package in that it +provides three builders: +* `mobile` +* `desktop` +* `web` + +This allows developers to build different widgets for their generalized platform targets + (Android + iOS = mobile, macOS + Windows + Linux = desktop). This is advantageous when requirements call for one set +of widgets that are identical across mobile, a different set of widgets that are identical across desktop, and another +that are required for web. + +### `DesktopWidgetBuilder` +This widget is more specialized than `PlatformWidgetBuilder` in that it allows for more targeted widget-building for +desktop platforms. It provides three builders: +* `macOS` +* `windows` +* `linux` + +This allows developers to build different widgets for the various desktop platforms. This is advantageous when building +native-looking desktop applications using the `macos_ui`, `fluent_ui`, and `yaru_widgets` packages. diff --git a/packages/stream_chat_flutter/lib/platform_widget_builder/platform_widget_builder.dart b/packages/stream_chat_flutter/lib/platform_widget_builder/platform_widget_builder.dart new file mode 100644 index 00000000..712d8798 --- /dev/null +++ b/packages/stream_chat_flutter/lib/platform_widget_builder/platform_widget_builder.dart @@ -0,0 +1,4 @@ +library platform_widget_builder; + +export 'src/desktop_widget_builder.dart'; +export 'src/platform_widget_builder.dart'; diff --git a/packages/stream_chat_flutter/lib/platform_widget_builder/src/desktop_widget.dart b/packages/stream_chat_flutter/lib/platform_widget_builder/src/desktop_widget.dart new file mode 100644 index 00000000..349b9d47 --- /dev/null +++ b/packages/stream_chat_flutter/lib/platform_widget_builder/src/desktop_widget.dart @@ -0,0 +1,38 @@ +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/platform_widget_builder/src/desktop_widget_base.dart'; + +/// A widget that will only be built for the specified desktop Platforms. +/// +/// See [DesktopWidgetBuilder] and [DesktopWidgetBase] for more. +/// +/// Also see: [PlatformWidget] and [PlatformWidgetBase]. +class DesktopWidget extends DesktopWidgetBase { + /// Builds a [DesktopWidget]. + const DesktopWidget({ + super.key, + this.macOS, + this.windows, + this.linux, + }); + + /// The widget to build for macOS. + final PlatformBuilder? macOS; + + /// The widget to build for Windows. + final PlatformBuilder? windows; + + /// The widget to build for Linux. + final PlatformBuilder? linux; + + @override + Widget createMacosWidget(BuildContext context) => + macOS?.call(context) ?? const SizedBox.shrink(); + + @override + Widget createWindowsWidget(BuildContext context) => + windows?.call(context) ?? const SizedBox.shrink(); + + @override + Widget createLinuxWidget(BuildContext context) => + linux?.call(context) ?? const SizedBox.shrink(); +} diff --git a/packages/stream_chat_flutter/lib/platform_widget_builder/src/desktop_widget_base.dart b/packages/stream_chat_flutter/lib/platform_widget_builder/src/desktop_widget_base.dart new file mode 100644 index 00000000..f5a42a7b --- /dev/null +++ b/packages/stream_chat_flutter/lib/platform_widget_builder/src/desktop_widget_base.dart @@ -0,0 +1,53 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart' show Theme; +import 'package:flutter/widgets.dart'; + +/// A generic widget builder function. +typedef PlatformBuilder = T Function( + BuildContext context, +); + +/// An abstract class used as a building block for creating +/// [DesktopPlatformWidget]s. +/// +/// This class is similar to [PlatformWidgetBase], which broadly covers +/// the platform categories; it combines the desktop platforms into a single +/// "desktop" target, for the purpose of returning the same widget one time +/// for all platforms in that category. [DesktopWidgetBase] differs in that +/// it more specifically targets each platform in the "desktop" category. +/// +/// This class utilizes generics to define the types of widgets it expects to +/// build: +/// * M = macOS +/// * W = Windows +/// * L = Linux +abstract class DesktopWidgetBase extends StatelessWidget { + /// Builds a [DesktopWidgetBase]. + const DesktopWidgetBase({super.key}); + + @override + Widget build(BuildContext context) { + final platform = Theme.of(context).platform; + if (platform == TargetPlatform.macOS) { + return createMacosWidget(context); + } else if (platform == TargetPlatform.windows) { + return createWindowsWidget(context); + } else if (platform == TargetPlatform.linux) { + return createLinuxWidget(context); + } + + return throw UnsupportedError( + 'This platform is not supported: $defaultTargetPlatform', + ); + } + + /// Builds a `M` macOS widget. + M createMacosWidget(BuildContext context); + + /// Builds a `W` Windows widget. + W createWindowsWidget(BuildContext context); + + /// Builds a `L` Linux widget. + L createLinuxWidget(BuildContext context); +} diff --git a/packages/stream_chat_flutter/lib/platform_widget_builder/src/desktop_widget_builder.dart b/packages/stream_chat_flutter/lib/platform_widget_builder/src/desktop_widget_builder.dart new file mode 100644 index 00000000..92367861 --- /dev/null +++ b/packages/stream_chat_flutter/lib/platform_widget_builder/src/desktop_widget_builder.dart @@ -0,0 +1,51 @@ +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/platform_widget_builder/src/desktop_widget.dart'; + +/// A widget-building function that includes the child widget. +typedef DesktopTargetBuilder = Widget? Function( + BuildContext context, + Widget? child, +)?; + +/// A widget that utilizes [DesktopWidgetBuilder]s to build different widgets +/// for each specified desktop platform. +/// +/// Usage: +/// ``` +/// DesktopWidgetBuilder( +/// macOS: (context, child) => MacosWidget(), +/// windows: (context, child) => WindowsWidget(), +/// linux: (context, child) => LinuxWidget(), +/// ), +/// ``` +class DesktopWidgetBuilder extends StatelessWidget { + /// Builds a [DesktopWidgetBuilder]. + const DesktopWidgetBuilder({ + super.key, + this.child, + this.macOS, + this.windows, + this.linux, + }); + + /// The child widget. + final Widget? child; + + /// The widget to build for macOS. + final DesktopTargetBuilder? macOS; + + /// The widget to build for windows. + final DesktopTargetBuilder? windows; + + /// The widget to build for linux. + final DesktopTargetBuilder? linux; + + @override + Widget build(BuildContext context) { + return DesktopWidget( + macOS: (context) => macOS?.call(context, child), + windows: (context) => windows?.call(context, child), + linux: (context) => linux?.call(context, child), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/platform_widget_builder/src/platform_widget.dart b/packages/stream_chat_flutter/lib/platform_widget_builder/src/platform_widget.dart new file mode 100644 index 00000000..90789d86 --- /dev/null +++ b/packages/stream_chat_flutter/lib/platform_widget_builder/src/platform_widget.dart @@ -0,0 +1,38 @@ +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/platform_widget_builder/src/platform_widget_base.dart'; + +/// A widget that will only be built for the specific Platforms: +/// +/// See [PlatformWidgetBuilder] and [PlatformWidgetBase] for more. +/// +/// Also see: [DesktopWidget] and [DesktopWidgetBase]. +class PlatformWidget extends PlatformWidgetBase { + /// Builds a [PlatformWidget]. + const PlatformWidget({ + super.key, + this.desktop, + this.mobile, + this.web, + }); + + /// The mobile widget to build. + final PlatformBuilder? mobile; + + /// The desktop widget to build. + final PlatformBuilder? desktop; + + /// The web widget to build. + final PlatformBuilder? web; + + @override + Widget createDesktopWidget(BuildContext context) => + desktop?.call(context) ?? const SizedBox.shrink(); + + @override + Widget createMobileWidget(BuildContext context) => + mobile?.call(context) ?? const SizedBox.shrink(); + + @override + Widget createWebWidget(BuildContext context) => + web?.call(context) ?? const SizedBox.shrink(); +} diff --git a/packages/stream_chat_flutter/lib/platform_widget_builder/src/platform_widget_base.dart b/packages/stream_chat_flutter/lib/platform_widget_builder/src/platform_widget_base.dart new file mode 100644 index 00000000..9d3470d6 --- /dev/null +++ b/packages/stream_chat_flutter/lib/platform_widget_builder/src/platform_widget_base.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart' show Theme; +import 'package:flutter/widgets.dart'; + +/// A generic widget builder function. +typedef PlatformBuilder = T Function( + BuildContext context, +); + +/// An abstract class used as a building block for creating [PlatformWidget]s. +/// +/// This class broadly covers the platforms by combining Android and iOS +/// together as a "mobile" category, macOS, Windows, and Linux together as a +/// "desktop" category. This is useful is cases where a widget is expected to +/// be the same for the various mobile and desktop categories, and would +/// therefore be tedious to return the same widget more than once for the +/// specified category. This is unlike [DesktopWidgetBase], which more +/// specifically targets the various platforms in the "desktop" category. +/// +/// This class utilizes generics to define the types of widgets it expects to +/// build: +/// * M = Mobile +/// * D = Desktop +/// * W = Web +abstract class PlatformWidgetBase extends StatelessWidget { + /// Builds a [PlatformWidgetBase]. + const PlatformWidgetBase({ + super.key, + }); + + @override + Widget build(BuildContext context) { + final platform = Theme.of(context).platform; + if (platform == TargetPlatform.android || platform == TargetPlatform.iOS) { + return createMobileWidget(context); + } else if (platform == TargetPlatform.macOS || + platform == TargetPlatform.windows || + platform == TargetPlatform.linux) { + return createDesktopWidget(context); + } else { + return createWebWidget(context); + } + } + + /// Builds a `M` mobile widget. + M createMobileWidget(BuildContext context); + + /// Builds a `D` desktop widget. + D createDesktopWidget(BuildContext context); + + /// Builds a `W` web widget. + W createWebWidget(BuildContext context); +} diff --git a/packages/stream_chat_flutter/lib/platform_widget_builder/src/platform_widget_builder.dart b/packages/stream_chat_flutter/lib/platform_widget_builder/src/platform_widget_builder.dart new file mode 100644 index 00000000..13b0a13c --- /dev/null +++ b/packages/stream_chat_flutter/lib/platform_widget_builder/src/platform_widget_builder.dart @@ -0,0 +1,50 @@ +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/platform_widget_builder/src/platform_widget.dart'; + +/// A widget-building function that includes the child widget. +typedef PlatformTargetBuilder = Widget? Function( + BuildContext context, + Widget? child, +)?; + +/// A widget that utilizes [PlatformTargetBuilder]s to build different widgets +/// for each specified platform. +/// +/// In the following real-world example, only the `mobile` builder is used +/// to ensure the child widget is only built for Android and iOS: +/// ``` +/// PlatformWidgetBuilder( +/// mobile: (context, child) => _buildFilePickerSection(), +/// ), +/// ``` +class PlatformWidgetBuilder extends StatelessWidget { + /// Builds a [PlatformWidgetBuilder]. + const PlatformWidgetBuilder({ + super.key, + this.child, + this.mobile, + this.desktop, + this.web, + }); + + /// The child widget. + final Widget? child; + + /// The widget to build for mobile platforms. + final PlatformTargetBuilder? mobile; + + /// The widget to build for desktop platforms. + final PlatformTargetBuilder? desktop; + + /// The widget to build for web platforms. + final PlatformTargetBuilder? web; + + @override + Widget build(BuildContext context) { + return PlatformWidget( + desktop: (context) => desktop?.call(context, child), + mobile: (context) => mobile?.call(context, child), + web: (context) => web?.call(context, child), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/item_positions_listener.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/item_positions_listener.dart index 1b611393..b58aae06 100644 --- a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/item_positions_listener.dart +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/item_positions_listener.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'package:flutter/foundation.dart'; - import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_notifier.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/src/scrollable_positioned_list.dart'; diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/item_positions_notifier.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/item_positions_notifier.dart index 3b3645cb..8759ddea 100644 --- a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/item_positions_notifier.dart +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/item_positions_notifier.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'package:flutter/foundation.dart'; - import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_listener.dart'; /// Internal implementation of [ItemPositionsListener]. diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/positioned_list.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/positioned_list.dart index b2549500..0814ef9a 100644 --- a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/positioned_list.dart +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/positioned_list.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; - import 'package:stream_chat_flutter/scrollable_positioned_list/src/element_registry.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/src/indexed_key.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_listener.dart'; diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scroll_view.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scroll_view.dart index 1aff0df3..8aebfb23 100644 --- a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scroll_view.dart +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scroll_view.dart @@ -5,7 +5,6 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; - import 'package:stream_chat_flutter/scrollable_positioned_list/src/viewport.dart'; /// {@template custom_scroll_view} diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart index d38b4255..72ebb659 100644 --- a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart @@ -8,7 +8,6 @@ import 'dart:math'; import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; - import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_listener.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_notifier.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/src/positioned_list.dart'; diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment.dart index 5550004d..fc6dabab 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment.dart @@ -1,5 +1,7 @@ +export 'attachment_error.dart'; +export 'attachment_error.dart'; export 'attachment_upload_state_builder.dart'; -export 'attachment_widget.dart' show AttachmentError, AttachmentSource; +export 'attachment_widget.dart' show AttachmentSource; export 'file_attachment.dart'; export 'giphy_attachment.dart'; export 'image_attachment.dart'; diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_error.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_error.dart new file mode 100644 index 00000000..85e806e9 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_error.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template attachmentError} +/// Widget for building in case of error +/// {@endtemplate} +class AttachmentError extends StatelessWidget { + /// {@macro attachmentError} + const AttachmentError({ + super.key, + this.constraints, + }); + + /// constraints of error + final BoxConstraints? constraints; + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + constraints: constraints ?? const BoxConstraints.expand(), + color: + StreamChatTheme.of(context).colorTheme.accentError.withOpacity(0.1), + child: Center( + child: Icon( + Icons.error_outline, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, + ), + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart index 831053b3..dc9c34c1 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart @@ -1,15 +1,11 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@macro attachment_title} -@Deprecated("Use 'StreamAttachmentTitle' instead") -typedef AttachmentTitle = StreamAttachmentTitle; - -/// {@template attachment_title} +/// {@template attachmentTitle} /// Title for attachments /// {@endtemplate} class StreamAttachmentTitle extends StatelessWidget { - /// Supply attachment and theme for constructing title + /// {@macro attachmentTitle} const StreamAttachmentTitle({ super.key, required this.attachment, diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart index 822e7e79..3a0d4ca7 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart @@ -1,50 +1,39 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/upload_progress_indicator.dart'; +import 'package:stream_chat_flutter/src/utils/utils.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// Widget to build in progress -typedef InProgressBuilder = Widget Function(BuildContext, int, int); - -/// Widget to build on failure -typedef FailedBuilder = Widget Function(BuildContext, String); - -/// {@macro attachment_upload_state_builder} -@Deprecated("Use 'StreamAttachmentsUploadStateBuilder' instead") -typedef AttachmentUploadStateBuilder = StreamAttachmentUploadStateBuilder; - -/// {@template attachment_upload_state_builder} +/// {@template streamAttachmentUploadStateBuilder} /// Widget to display attachment upload state /// {@endtemplate} class StreamAttachmentUploadStateBuilder extends StatelessWidget { - /// Constructor for creating an [StreamAttachmentUploadStateBuilder] widget + /// {@macro streamAttachmentUploadStateBuilder} const StreamAttachmentUploadStateBuilder({ super.key, required this.message, required this.attachment, - this.failedBuilder, - this.successBuilder, - this.inProgressBuilder, this.preparingBuilder, + this.inProgressBuilder, + this.successBuilder, + this.failedBuilder, }); - /// Message which attachment is added to + /// The message that [attachment] is associated with final Message message; - /// Attachment in concern + /// The attachment currently being handled final Attachment attachment; - /// Widget to display when failed - final FailedBuilder? failedBuilder; + /// Widget to display when preparing to upload the [attachment] + final PreparingBuilder? preparingBuilder; - /// Widget to display when succeeded - final WidgetBuilder? successBuilder; - - /// Widget to display when in progress + /// {@macro inProgressBuilder} final InProgressBuilder? inProgressBuilder; - /// Widget to display when in prep - final WidgetBuilder? preparingBuilder; + /// {@macro successBuilder} + final SuccessBuilder? successBuilder; + + /// {@macro failedBuilder} + final FailedBuilder? failedBuilder; @override Widget build(BuildContext context) { @@ -56,18 +45,22 @@ class StreamAttachmentUploadStateBuilder extends StatelessWidget { final attachmentId = attachment.id; final inProgress = inProgressBuilder ?? - (context, int sent, int total) => _InProgressState( - sent: sent, - total: total, - attachmentId: attachmentId, - ); + (context, int sent, int total) { + return _InProgressState( + sent: sent, + total: total, + attachmentId: attachmentId, + ); + }; final failed = failedBuilder ?? - (context, error) => _FailedState( - error: error, - messageId: messageId, - attachmentId: attachmentId, - ); + (context, error) { + return _FailedState( + error: error, + messageId: messageId, + attachmentId: attachmentId, + ); + }; final success = successBuilder ?? (context) => _SuccessState(); @@ -93,22 +86,24 @@ class _IconButton extends StatelessWidget { final VoidCallback? onPressed; @override - Widget build(BuildContext context) => SizedBox( - height: 24, - width: 24, - child: RawMaterialButton( - elevation: 0, - highlightElevation: 0, - focusElevation: 0, - hoverElevation: 0, - onPressed: onPressed, - fillColor: StreamChatTheme.of(context).colorTheme.overlayDark, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: icon, + Widget build(BuildContext context) { + return SizedBox( + height: 24, + width: 24, + child: RawMaterialButton( + elevation: 0, + highlightElevation: 0, + focusElevation: 0, + hoverElevation: 0, + onPressed: onPressed, + fillColor: StreamChatTheme.of(context).colorTheme.overlayDark, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), ), - ); + child: icon, + ), + ); + } } class _PreparingState extends StatelessWidget { @@ -237,14 +232,16 @@ class _FailedState extends StatelessWidget { class _SuccessState extends StatelessWidget { @override - Widget build(BuildContext context) => Align( - alignment: Alignment.topRight, - child: CircleAvatar( - backgroundColor: StreamChatTheme.of(context).colorTheme.overlayDark, - maxRadius: 12, - child: StreamSvgIcon.check( - color: StreamChatTheme.of(context).colorTheme.barsBg, - ), + Widget build(BuildContext context) { + return Align( + alignment: Alignment.topRight, + child: CircleAvatar( + backgroundColor: StreamChatTheme.of(context).colorTheme.overlayDark, + maxRadius: 12, + child: StreamSvgIcon.check( + color: StreamChatTheme.of(context).colorTheme.barsBg, ), - ); + ), + ); + } } diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart index bf1d0f67..182d5aea 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart @@ -24,31 +24,28 @@ enum AttachmentSource { } } -/// {@macro attachment_widget} -@Deprecated("Use 'StreamAttachmentWidget' instead") -typedef AttachmentWidget = StreamAttachmentWidget; - -/// {@template attachment_widget} +/// {@template streamAttachmentWidget} /// Abstract class for deriving attachment types /// {@endtemplate} abstract class StreamAttachmentWidget extends StatelessWidget { - /// Constructor for creating attachment widget + /// {@macro streamAttachmentWidget} const StreamAttachmentWidget({ super.key, required this.message, required this.attachment, - this.size, + this.constraints, AttachmentSource? source, }) : _source = source; - /// Size of attachments - final Size? size; + /// Contraints of attachments + final BoxConstraints? constraints; + final AttachmentSource? _source; - /// Message which attachment is attached to + /// The message that [attachment] is associated with final Message message; - /// Attachment to display + /// The [Attachment] to display final Attachment attachment; /// Getter for source of attachment @@ -58,33 +55,3 @@ abstract class StreamAttachmentWidget extends StatelessWidget { ? AttachmentSource.local : AttachmentSource.network); } - -/// Widget for building in case of error -class AttachmentError extends StatelessWidget { - /// Constructor for creating AttachmentError - const AttachmentError({ - super.key, - this.size, - }); - - /// Size of error - final Size? size; - - @override - Widget build(BuildContext context) => Center( - child: Container( - width: size?.width, - height: size?.height, - color: StreamChatTheme.of(context) - .colorTheme - .accentError - .withOpacity(0.1), - child: Center( - child: Icon( - Icons.error_outline, - color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, - ), - ), - ), - ); -} diff --git a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart index 2feeb0c2..ff11fd46 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart @@ -2,46 +2,45 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; -import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; -import 'package:stream_chat_flutter/src/upload_progress_indicator.dart'; -import 'package:stream_chat_flutter/src/utils.dart'; -import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; +import 'package:stream_chat_flutter/src/attachment/handler/stream_attachment_handler.dart'; +import 'package:stream_chat_flutter/src/indicators/upload_progress_indicator.dart'; +import 'package:stream_chat_flutter/src/misc/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/utils/utils.dart'; +import 'package:stream_chat_flutter/src/video/video_thumbnail_image.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -/// {@macro file_attachment} -@Deprecated("Use 'StreamFileAttachment' instead") -typedef FileAttachment = StreamFileAttachment; - -/// {@template file_attachment} -/// Widget for displaying file attachments +/// {@template streamFileAttachment} +/// Displays file attachments that have been sent in a chat. +/// +/// Used in [MessageWidget]. /// {@endtemplate} class StreamFileAttachment extends StreamAttachmentWidget { - /// Constructor for creating a widget when attachment is of type 'file' + /// {@macro streamFileAttachment} const StreamFileAttachment({ super.key, required super.message, required super.attachment, - super.size, + super.constraints, this.title, this.trailing, this.onAttachmentTap, }); - /// Title for attachment + /// Title for the attachment final Widget? title; - /// Widget for displaying at the end of attachment (such as a download button) + /// Widget for displaying at the end of the attachment + /// (such as a download button) final Widget? trailing; - /// Callback called when attachment widget is tapped - final VoidCallback? onAttachmentTap; + /// {@macro onAttachmentTap} + final OnAttachmentTap? onAttachmentTap; - /// Check if attachment is a video + /// Checks if the attachment is a video bool get isVideoAttachment => attachment.title?.mimeType?.type == 'video'; - /// Check if attachment is an image + /// Checks if the attachment is an image bool get isImageAttachment => attachment.title?.mimeType?.type == 'image'; @override @@ -51,7 +50,7 @@ class StreamFileAttachment extends StreamAttachmentWidget { child: GestureDetector( onTap: onAttachmentTap, child: Container( - width: size?.width ?? 100, + constraints: constraints ?? const BoxConstraints.tightFor(width: 100), height: 56, decoration: BoxDecoration( color: colorTheme.barsBg, @@ -67,7 +66,12 @@ class StreamFileAttachment extends StreamAttachmentWidget { height: 40, width: 33.33, margin: const EdgeInsets.all(8), - child: _getFileTypeImage(context), + child: _FileTypeImage( + isImageAttachment: isImageAttachment, + isVideoAttachment: isVideoAttachment, + source: source, + attachment: attachment, + ), ), const SizedBox(width: 8), Expanded( @@ -82,27 +86,51 @@ class StreamFileAttachment extends StreamAttachmentWidget { overflow: TextOverflow.ellipsis, ), const SizedBox(height: 3), - _buildSubtitle(context), + _FileAttachmentSubtitle(attachment: attachment), ], ), ), const SizedBox(width: 8), - _buildTrailing(context), + Material( + type: MaterialType.transparency, + child: trailing ?? + _Trailing( + attachment: attachment, + message: message, + ), + ), ], ), ), ), ); } +} - ShapeBorder _getDefaultShape(BuildContext context) => RoundedRectangleBorder( - side: const BorderSide(width: 0, color: Colors.transparent), - borderRadius: BorderRadius.circular(8), - ); +class _FileTypeImage extends StatelessWidget { + const _FileTypeImage({ + required this.isImageAttachment, + required this.isVideoAttachment, + required this.source, + required this.attachment, + }); - // TODO: Improve image memory. This is using the full image instead of a - // smaller version (thumbnail) - Widget _getFileTypeImage(BuildContext context) { + final bool isImageAttachment; + final bool isVideoAttachment; + final AttachmentSource source; + final Attachment attachment; + + ShapeBorder _getDefaultShape(BuildContext context) { + return RoundedRectangleBorder( + side: const BorderSide(width: 0, color: Colors.transparent), + borderRadius: BorderRadius.circular(8), + ); + } + + // TODO: Improve image memory. + // This is using the full image instead of a smaller version (thumbnail) + @override + Widget build(BuildContext context) { if (isImageAttachment) { return Material( clipBehavior: Clip.hardEdge, @@ -111,13 +139,16 @@ class StreamFileAttachment extends StreamAttachmentWidget { child: source.when( local: () { if (attachment.file?.bytes == null) { - return getFileTypeImage(attachment.extraData['other'] as String?); + return getFileTypeImage( + attachment.extraData['mime_type'] as String?, + ); } return Image.memory( attachment.file!.bytes!, fit: BoxFit.cover, - errorBuilder: (_, obj, trace) => - getFileTypeImage(attachment.extraData['other'] as String?), + errorBuilder: (_, obj, trace) => getFileTypeImage( + attachment.extraData['mime_type'] as String?, + ), ); }, network: () { @@ -125,15 +156,18 @@ class StreamFileAttachment extends StreamAttachmentWidget { attachment.assetUrl ?? attachment.thumbUrl) == null) { - return getFileTypeImage(attachment.extraData['other'] as String?); + return getFileTypeImage( + attachment.extraData['mime_type'] as String?, + ); } return CachedNetworkImage( imageUrl: attachment.imageUrl ?? attachment.assetUrl ?? attachment.thumbUrl!, fit: BoxFit.cover, - errorWidget: (_, obj, trace) => - getFileTypeImage(attachment.extraData['other'] as String?), + errorWidget: (_, obj, trace) => getFileTypeImage( + attachment.extraData['mime_type'] as String?, + ), placeholder: (_, __) { final image = Image.asset( 'images/placeholder.png', @@ -187,38 +221,47 @@ class StreamFileAttachment extends StreamAttachmentWidget { } return getFileTypeImage(attachment.extraData['mime_type'] as String?); } +} - Widget _buildButton({ - Widget? icon, - double iconSize = 24.0, - VoidCallback? onPressed, - Color? fillColor, - }) => - SizedBox( - height: iconSize, - width: iconSize, - child: RawMaterialButton( - elevation: 0, - highlightElevation: 0, - focusElevation: 0, - hoverElevation: 0, - onPressed: onPressed, - fillColor: fillColor, - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - child: icon, - ), - ); +class _Trailing extends StatelessWidget { + const _Trailing({ + required this.attachment, + required this.message, + }); - Widget _buildTrailing(BuildContext context) { + final Attachment attachment; + final Message message; + + @override + Widget build(BuildContext context) { final theme = StreamChatTheme.of(context); final channel = StreamChannel.of(context).channel; final attachmentId = attachment.id; - var trailingWidget = trailing; - trailingWidget ??= attachment.uploadState.when( + + if (message.status == MessageSendingStatus.sent) { + return IconButton( + icon: StreamSvgIcon.cloudDownload( + color: theme.colorTheme.textHighEmphasis, + ), + visualDensity: VisualDensity.compact, + splashRadius: 16, + onPressed: () async { + final assetUrl = attachment.assetUrl; + if (assetUrl != null) { + if (isMobileDeviceOrWeb) { + launchURL(context, assetUrl); + } else { + StreamAttachmentHandler.instance.downloadAttachment(attachment); + } + } + }, + ); + } + + return attachment.uploadState.when( preparing: () => Padding( padding: const EdgeInsets.all(8), - child: _buildButton( + child: _TrailingButton( icon: StreamSvgIcon.close(color: theme.colorTheme.barsBg), fillColor: theme.colorTheme.overlayDark, onPressed: () => channel.cancelAttachmentUpload(attachmentId), @@ -226,7 +269,7 @@ class StreamFileAttachment extends StreamAttachmentWidget { ), inProgress: (_, __) => Padding( padding: const EdgeInsets.all(8), - child: _buildButton( + child: _TrailingButton( icon: StreamSvgIcon.close(color: theme.colorTheme.barsBg), fillColor: theme.colorTheme.overlayDark, onPressed: () => channel.cancelAttachmentUpload(attachmentId), @@ -242,7 +285,7 @@ class StreamFileAttachment extends StreamAttachmentWidget { ), failed: (_) => Padding( padding: const EdgeInsets.all(8), - child: _buildButton( + child: _TrailingButton( icon: StreamSvgIcon.retry(color: theme.colorTheme.barsBg), fillColor: theme.colorTheme.overlayDark, onPressed: () => channel.retryAttachmentUpload( @@ -252,28 +295,48 @@ class StreamFileAttachment extends StreamAttachmentWidget { ), ), ); + } +} - if (message.status == MessageSendingStatus.sent) { - trailingWidget = IconButton( - icon: StreamSvgIcon.cloudDownload( - color: theme.colorTheme.textHighEmphasis, - ), - visualDensity: VisualDensity.compact, - splashRadius: 16, - onPressed: () { - final assetUrl = attachment.assetUrl; - if (assetUrl != null) launchURL(context, assetUrl); - }, - ); - } +class _TrailingButton extends StatelessWidget { + const _TrailingButton({ + this.onPressed, + this.fillColor, + this.icon, + }); - return Material( - type: MaterialType.transparency, - child: trailingWidget, + final VoidCallback? onPressed; + final Color? fillColor; + final Widget? icon; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 24, + width: 24, + child: RawMaterialButton( + elevation: 0, + highlightElevation: 0, + focusElevation: 0, + hoverElevation: 0, + onPressed: onPressed, + fillColor: fillColor, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: icon, + ), ); } +} - Widget _buildSubtitle(BuildContext context) { +class _FileAttachmentSubtitle extends StatelessWidget { + const _FileAttachmentSubtitle({ + required this.attachment, + }); + + final Attachment attachment; + + @override + Widget build(BuildContext context) { final theme = StreamChatTheme.of(context); final size = attachment.file?.size ?? attachment.extraData['file_size']; final textStyle = theme.textTheme.footnote.copyWith( diff --git a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart index f788b351..c6ef2b8d 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart @@ -2,36 +2,32 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/utils/utils.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@macro giphy_attachment} -@Deprecated("Use 'StreamGiphyAttachment' instead") -typedef GiphyAttachment = StreamGiphyAttachment; - -/// {@template giphy_attachment} -/// Widget for showing a GIF attachment +/// {@template streamGiphyAttachment} +/// Shows a GIF attachment in a [StreamMessageWidget]. /// {@endtemplate} class StreamGiphyAttachment extends StreamAttachmentWidget { - /// Constructor for creating a [StreamGiphyAttachment] widget + /// {@macro streamGiphyAttachment} const StreamGiphyAttachment({ super.key, required super.message, required super.attachment, - super.size, + super.constraints, this.onShowMessage, - this.onReturnAction, + this.onReplyMessage, this.onAttachmentTap, }); - /// Callback when show message is tapped + /// {@macro showMessageCallback} final ShowMessageCallback? onShowMessage; - /// Callback when attachment is returned to from other screens - final ValueChanged? onReturnAction; + /// {@macro replyMessageCallback} + final ReplyMessageCallback? onReplyMessage; - /// Callback when attachment is tapped - final VoidCallback? onAttachmentTap; + /// {@macro onAttachmentTap} + final OnAttachmentTap? onAttachmentTap; @override Widget build(BuildContext context) { @@ -48,287 +44,301 @@ class StreamGiphyAttachment extends StreamAttachmentWidget { Widget _buildSendingAttachment(BuildContext context, String imageUrl) { final streamChannel = StreamChannel.of(context); - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Card( - color: StreamChatTheme.of(context).colorTheme.barsBg, - elevation: 2, - clipBehavior: Clip.hardEdge, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topRight: Radius.circular(16), - topLeft: Radius.circular(16), - bottomLeft: Radius.circular(16), + return ConstrainedBox( + constraints: constraints?.copyWith( + maxHeight: double.infinity, + ) ?? + const BoxConstraints.expand(), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Card( + color: StreamChatTheme.of(context).colorTheme.barsBg, + elevation: 2, + clipBehavior: Clip.hardEdge, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular(16), + topLeft: Radius.circular(16), + bottomLeft: Radius.circular(16), + ), ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.all(8), - child: Row( - children: [ - StreamSvgIcon.giphyIcon(), - const SizedBox(width: 8), - Text( - context.translations.giphyLabel, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(width: 8), - if (attachment.title != null) - Flexible( - child: Text( - attachment.title!, - style: TextStyle( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + StreamSvgIcon.giphyIcon(), + const SizedBox(width: 8), + Text( + context.translations.giphyLabel, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(width: 8), + if (attachment.title != null) + Flexible( + child: Text( + attachment.title!, + style: TextStyle( + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(0.5), + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, ), - overflow: TextOverflow.ellipsis, - maxLines: 1, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(2), + child: GestureDetector( + onTap: () { + if (onAttachmentTap != null) { + onAttachmentTap?.call(); + } else { + _onImageTap(context); + } + }, + child: CachedNetworkImage( + height: constraints?.maxHeight, + width: constraints?.maxWidth, + placeholder: (_, __) => SizedBox( + width: constraints?.maxHeight, + height: constraints?.maxWidth, + child: const Center( + child: CircularProgressIndicator(), ), ), + imageUrl: imageUrl, + errorWidget: (context, url, error) => AttachmentError( + constraints: constraints, + ), + fit: BoxFit.cover, + ), + ), + ), + Container( + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(0.2), + width: double.infinity, + height: 0.5, + ), + Row( + children: [ + Expanded( + child: SizedBox( + height: 50, + child: TextButton( + onPressed: () { + streamChannel.channel.sendAction( + message, + { + 'image_action': 'cancel', + }, + ); + }, + child: Text( + context.translations.cancelLabel + .toLowerCase() + .capitalize(), + style: StreamChatTheme.of(context) + .textTheme + .bodyBold + .copyWith( + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(0.5), + ), + ), + ), + ), + ), + Container( + width: 0.5, + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(0.2), + height: 50, + ), + Expanded( + child: SizedBox( + height: 50, + child: TextButton( + onPressed: () { + streamChannel.channel.sendAction( + message, + { + 'image_action': 'shuffle', + }, + ); + }, + child: Text( + context.translations.shuffleLabel, + style: StreamChatTheme.of(context) + .textTheme + .bodyBold + .copyWith( + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(0.5), + ), + maxLines: 1, + ), + ), + ), + ), + Container( + width: 0.5, + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(0.2), + height: 50, + ), + Expanded( + child: SizedBox( + height: 50, + child: TextButton( + onPressed: () { + streamChannel.channel.sendAction( + message, + { + 'image_action': 'send', + }, + ); + }, + child: Text( + context.translations.sendLabel, + style: TextStyle( + color: StreamChatTheme.of(context) + .colorTheme + .accentPrimary, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), ], ), - ), - Padding( - padding: const EdgeInsets.all(2), - child: GestureDetector( - onTap: () { - if (onAttachmentTap != null) { - onAttachmentTap?.call(); - } else { - _onImageTap(context); - } - }, - child: CachedNetworkImage( - height: size?.height, - width: size?.width, - placeholder: (_, __) => SizedBox( - width: size?.width, - height: size?.height, - child: const Center( - child: CircularProgressIndicator(), - ), - ), - imageUrl: imageUrl, - errorWidget: (context, url, error) => - AttachmentError(size: size), - fit: BoxFit.cover, - ), - ), - ), - Container( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.2), - width: double.infinity, - height: 0.5, - ), - Row( - children: [ - Expanded( - child: SizedBox( - height: 50, - child: TextButton( - onPressed: () { - streamChannel.channel.sendAction(message, { - 'image_action': 'cancel', - }); - }, - child: Text( - context.translations.cancelLabel - .toLowerCase() - .capitalize(), - style: StreamChatTheme.of(context) - .textTheme - .bodyBold - .copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5), - ), - ), - ), - ), - ), - Container( - width: 0.5, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.2), - height: 50, - ), - Expanded( - child: SizedBox( - height: 50, - child: TextButton( - onPressed: () { - streamChannel.channel.sendAction(message, { - 'image_action': 'shuffle', - }); - }, - child: Text( - context.translations.shuffleLabel, - style: StreamChatTheme.of(context) - .textTheme - .bodyBold - .copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5), - ), - maxLines: 1, - ), - ), - ), - ), - Container( - width: 0.5, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.2), - height: 50, - ), - Expanded( - child: SizedBox( - height: 50, - child: TextButton( - onPressed: () { - streamChannel.channel.sendAction(message, { - 'image_action': 'send', - }); - }, - child: Text( - context.translations.sendLabel, - style: TextStyle( - color: StreamChatTheme.of(context) - .colorTheme - .accentPrimary, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ), - ], - ), - ], + ], + ), ), - ), - const SizedBox(height: 4), - const Align( - alignment: Alignment.centerRight, - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), - child: StreamVisibleFootnote(), + const SizedBox(height: 4), + const Align( + alignment: Alignment.centerRight, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: StreamVisibleFootnote(), + ), ), - ), - ], + ], + ), ); } - void _onImageTap(BuildContext context) async { - final res = await Navigator.push( - context, + Future _onImageTap(BuildContext context) async { + await Navigator.of(context).push( MaterialPageRoute( builder: (_) { final channel = StreamChannel.of(context).channel; return StreamChannel( channel: channel, - child: StreamFullScreenMedia( + child: StreamFullScreenMediaBuilder( mediaAttachmentPackages: message.getAttachmentPackageList(), startIndex: message.attachments.indexOf(attachment), - userName: message.user?.name, + userName: message.user!.name, onShowMessage: onShowMessage, + onReplyMessage: onReplyMessage, ), ); }, ), ); - if (res != null) onReturnAction?.call(res); } - Widget _buildSentAttachment(BuildContext context, String imageUrl) => - SizedBox( - child: GestureDetector( - onTap: () { - if (onAttachmentTap != null) { - onAttachmentTap?.call(); - } else { - _onImageTap(context); - } - }, - child: Stack( - children: [ - CachedNetworkImage( - height: size?.height, - width: size?.width, - placeholder: (_, __) { - final image = Image.asset( - 'images/placeholder.png', - fit: BoxFit.cover, - package: 'stream_chat_flutter', - ); - - final colorTheme = StreamChatTheme.of(context).colorTheme; - return Shimmer.fromColors( - baseColor: colorTheme.disabled, - highlightColor: colorTheme.inputBg, - child: image, - ); - }, - imageUrl: imageUrl, - errorWidget: (context, url, error) => - AttachmentError(size: size), + Widget _buildSentAttachment(BuildContext context, String imageUrl) { + return GestureDetector( + onTap: () { + if (onAttachmentTap != null) { + onAttachmentTap?.call(); + } else { + _onImageTap(context); + } + }, + child: Stack( + children: [ + CachedNetworkImage( + height: constraints?.maxHeight, + width: constraints?.maxWidth, + placeholder: (_, __) { + final image = Image.asset( + 'images/placeholder.png', fit: BoxFit.cover, + package: 'stream_chat_flutter', + ); + + final colorTheme = StreamChatTheme.of(context).colorTheme; + return Shimmer.fromColors( + baseColor: colorTheme.disabled, + highlightColor: colorTheme.inputBg, + child: image, + ); + }, + imageUrl: imageUrl, + errorWidget: (context, url, error) => AttachmentError( + constraints: constraints, + ), + fit: BoxFit.cover, + ), + Positioned( + bottom: 8, + left: 8, + child: Material( + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(0.5), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), ), - Positioned( - bottom: 8, - left: 8, - child: Material( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + child: Row( + children: [ + StreamSvgIcon.lightning( + color: StreamChatTheme.of(context).colorTheme.barsBg, + size: 16, ), - child: Row( - children: [ - StreamSvgIcon.lightning( - color: StreamChatTheme.of(context).colorTheme.barsBg, - size: 16, - ), - Text( - context.translations.giphyLabel.toUpperCase(), - style: TextStyle( - color: - StreamChatTheme.of(context).colorTheme.barsBg, - fontWeight: FontWeight.bold, - fontSize: 11, - ), - ), - ], + Text( + context.translations.giphyLabel.toUpperCase(), + style: TextStyle( + color: StreamChatTheme.of(context).colorTheme.barsBg, + fontWeight: FontWeight.bold, + fontSize: 11, + ), ), - ), + ], ), ), - ], + ), ), - ), - ); + ], + ), + ); + } } diff --git a/packages/stream_chat_flutter/lib/src/attachment/handler/common.dart b/packages/stream_chat_flutter/lib/src/attachment/handler/common.dart new file mode 100644 index 00000000..fb4f192a --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/attachment/handler/common.dart @@ -0,0 +1,71 @@ +import 'dart:typed_data'; + +import 'package:dio/dio.dart'; +import 'package:file_selector/file_selector.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// Downloads the [attachment] to the device and returns +/// the path to the file. +Future downloadWebOrDesktopAttachment( + Attachment attachment, { + ProgressCallback? onReceiveProgress, + Map? queryParameters, + CancelToken? cancelToken, + Options? options, +}) async { + final type = attachment.type; + + String? downloadUrl; + String? fileName; + /* ---IMAGES/GIFS--- */ + if (type == 'image') { + downloadUrl = attachment.imageUrl ?? attachment.assetUrl; + fileName = attachment.title; + fileName ??= 'attachment.${attachment.mimeType ?? 'png'}'; + } + /* ---GIPHY's--- */ + else if (type == 'giphy') { + downloadUrl = attachment.thumbUrl; + fileName = '${attachment.title}.gif'; + } + /* ---FILES AND VIDEOS--- */ + else if (type == 'file' || type == 'video') { + downloadUrl = attachment.assetUrl; + fileName = attachment.title; + } + + assert( + downloadUrl != null, + 'Attachment must have an assetUrl or imageUrl or thumbUrl', + ); + + final response = await Dio().get>( + downloadUrl!, + onReceiveProgress: onReceiveProgress, + queryParameters: queryParameters, + cancelToken: cancelToken, + // set responseType to `bytes` + options: options?.copyWith(responseType: ResponseType.bytes) ?? + Options(responseType: ResponseType.bytes), + ); + + // Open the native file browser so the user can select the download path. + final path = await getSavePath(suggestedName: fileName); + + if (path == null) { + // Operation was canceled by the user. + return null; + } + + // Create an XFile for proper file saving + final file = XFile.fromData( + Uint8List.fromList(response.data!), + mimeType: attachment.mimeType, + name: fileName, + path: path, + ); + + // Save the file to the user's selected path. + await file.saveTo(path); + return path; +} diff --git a/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler.dart b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler.dart new file mode 100644 index 00000000..36d45725 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler.dart @@ -0,0 +1,4 @@ +export 'stream_attachment_handler_base.dart' + if (dart.library.html) 'stream_attachment_handler_html.dart' + if (dart.library.io) 'stream_attachment_handler_io.dart' + show StreamAttachmentHandler; diff --git a/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_base.dart b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_base.dart new file mode 100644 index 00000000..7879f636 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_base.dart @@ -0,0 +1,80 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// Base class for handling attachment related functionality. +abstract class StreamAttachmentHandlerBase { + /// Pick an image from the device. + Future pickImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) { + throw UnimplementedError('pickImage is not implemented'); + } + + /// Pick a video from the device. + Future pickVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) { + throw UnimplementedError('pickVideo is not implemented'); + } + + /// Pick a file from the device. + Future pickFile({ + String? dialogTitle, + String? initialDirectory, + FileType type = FileType.any, + List? allowedExtensions, + Function(FilePickerStatus)? onFileLoading, + bool allowCompression = true, + bool withData = true, + bool withReadStream = false, + bool lockParentWindow = true, + }) { + throw UnimplementedError('pickFile is not implemented'); + } + + /// Pick an audio from the device. + Future pickAudio() { + throw UnimplementedError('pickAudio is not implemented'); + } + + /// Saves the [attachmentFile] to the temporary directory. + Future saveAttachmentFile({ + required AttachmentFile attachmentFile, + }) { + throw UnimplementedError('saveFile is not implemented'); + } + + /// Deletes the [attachmentFile] from the temporary directory. + Future deleteAttachmentFile({ + required AttachmentFile attachmentFile, + }) { + throw UnimplementedError('deleteAttachmentFile is not implemented'); + } + + /// Downloads the [attachment] to the device and returns + /// the path to the file. + Future downloadAttachment( + Attachment attachment, { + ProgressCallback? onReceiveProgress, + Map? queryParameters, + CancelToken? cancelToken, + Options? options, + }) { + throw UnimplementedError('downloadAttachment is not implemented'); + } +} + +/// Stub implementation of [StreamAttachmentHandlerBase]. +class StreamAttachmentHandler extends StreamAttachmentHandlerBase { + /// Returns an instance of [StreamAttachmentHandler]. + static StreamAttachmentHandler get instance { + throw UnimplementedError('instance is not implemented'); + } +} diff --git a/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_html.dart b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_html.dart new file mode 100644 index 00000000..6a9adcea --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_html.dart @@ -0,0 +1,63 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:stream_chat_flutter/src/attachment/handler/common.dart'; +import 'package:stream_chat_flutter/src/attachment/handler/stream_attachment_handler_base.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// StreamAttachmentHandler implementation for html. +class StreamAttachmentHandler extends StreamAttachmentHandlerBase { + StreamAttachmentHandler._(); + + static StreamAttachmentHandler? _instance; + + /// Returns the singleton instance of [StreamAttachmentHandler]. + // ignore: prefer_constructors_over_static_methods + static StreamAttachmentHandler get instance => + _instance ??= StreamAttachmentHandler._(); + + late final _filePicker = FilePicker.platform; + + @override + Future pickFile({ + String? dialogTitle, + String? initialDirectory, + FileType type = FileType.any, + List? allowedExtensions, + Function(FilePickerStatus)? onFileLoading, + bool allowCompression = true, + bool withData = true, + bool withReadStream = false, + bool lockParentWindow = true, + }) async { + final result = await _filePicker.pickFiles( + dialogTitle: dialogTitle, + initialDirectory: initialDirectory, + type: type, + allowedExtensions: allowedExtensions, + onFileLoading: onFileLoading, + allowCompression: allowCompression, + withData: withData, + withReadStream: withReadStream, + lockParentWindow: lockParentWindow, + ); + + return result?.files.first.toAttachment(type: type.toAttachmentType()); + } + + @override + Future downloadAttachment( + Attachment attachment, { + ProgressCallback? onReceiveProgress, + Map? queryParameters, + CancelToken? cancelToken, + Options? options, + }) { + return downloadWebOrDesktopAttachment( + attachment, + onReceiveProgress: onReceiveProgress, + queryParameters: queryParameters, + cancelToken: cancelToken, + options: options, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_io.dart b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_io.dart new file mode 100644 index 00000000..305021b4 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_io.dart @@ -0,0 +1,216 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:dio/dio.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:stream_chat_flutter/src/attachment/handler/common.dart'; +import 'package:stream_chat_flutter/src/attachment/handler/stream_attachment_handler_base.dart'; + +import 'package:stream_chat_flutter/src/utils/device_segmentation.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// StreamAttachmentHandler implementation for desktop. +class StreamAttachmentHandlerDesktop extends StreamAttachmentHandler { + /// Returns the singleton instance of [StreamAttachmentHandler]. + StreamAttachmentHandlerDesktop() : super.__(); + + @override + Future downloadAttachment( + Attachment attachment, { + ProgressCallback? onReceiveProgress, + Map? queryParameters, + CancelToken? cancelToken, + Options? options, + }) { + return downloadWebOrDesktopAttachment( + attachment, + onReceiveProgress: onReceiveProgress, + queryParameters: queryParameters, + cancelToken: cancelToken, + options: options, + ); + } +} + +/// StreamAttachmentHandler implementation for io. +class StreamAttachmentHandler extends StreamAttachmentHandlerBase { + StreamAttachmentHandler.__(); + + factory StreamAttachmentHandler._() { + if (isDesktopDevice) { + return StreamAttachmentHandlerDesktop(); + } + return StreamAttachmentHandler.__(); + } + + static StreamAttachmentHandler? _instance; + + /// Returns the singleton instance of [StreamAttachmentHandler]. + // ignore: prefer_constructors_over_static_methods + static StreamAttachmentHandler get instance => + _instance ??= StreamAttachmentHandler._(); + + late final _imagePicker = ImagePicker(); + late final _filePicker = FilePicker.platform; + + @override + Future pickImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) async { + final image = await _imagePicker.pickImage( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice, + ); + + return image?.toAttachment(type: 'image'); + } + + @override + Future pickVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + final video = await _imagePicker.pickVideo( + source: source, + preferredCameraDevice: preferredCameraDevice, + maxDuration: maxDuration, + ); + + return video?.toAttachment(type: 'video'); + } + + @override + Future pickFile({ + String? dialogTitle, + String? initialDirectory, + FileType type = FileType.any, + List? allowedExtensions, + Function(FilePickerStatus)? onFileLoading, + bool allowCompression = true, + bool withData = true, + bool withReadStream = false, + bool lockParentWindow = true, + }) async { + final result = await _filePicker.pickFiles( + dialogTitle: dialogTitle, + initialDirectory: initialDirectory, + type: type, + allowedExtensions: allowedExtensions, + onFileLoading: onFileLoading, + allowCompression: allowCompression, + withData: withData, + withReadStream: withReadStream, + lockParentWindow: lockParentWindow, + ); + + return result?.files.first.toAttachment(type: type.toAttachmentType()); + } + + @override + Future saveAttachmentFile({ + required AttachmentFile attachmentFile, + }) async { + final fileName = attachmentFile.name; + assert(fileName != null, 'Attachment file name is required'); + + final tempDir = await getTemporaryDirectory(); + final tempPath = Uri.file(tempDir.path, windows: CurrentPlatform.isWindows); + final tempFilePath = tempPath.resolve(fileName!).path; + print(tempFilePath); + + final attachmentFileBytes = attachmentFile.bytes; + if (attachmentFileBytes == null) { + final attachmentFilePath = attachmentFile.path!; + final file = File(attachmentFilePath); + return file.copy(tempFilePath).then((it) => it.path); + } else { + final file = File(tempFilePath); + return file.writeAsBytes(attachmentFileBytes).then((it) => it.path); + } + } + + @override + Future deleteAttachmentFile({ + required AttachmentFile attachmentFile, + }) async { + final attachmentFilePath = attachmentFile.path; + if (attachmentFilePath != null) { + final file = File(attachmentFilePath); + if (file.existsSync()) { + await file.delete(); + } + } + } + + @override + Future downloadAttachment( + Attachment attachment, { + ProgressCallback? onReceiveProgress, + Map? queryParameters, + CancelToken? cancelToken, + Options? options, + }) async { + final type = attachment.type; + + String? downloadUrl; + String? fileName; + /* ---IMAGES/GIFS--- */ + if (type == 'image') { + downloadUrl = attachment.imageUrl ?? attachment.assetUrl; + fileName = attachment.title; + fileName ??= 'attachment.${attachment.mimeType ?? 'png'}'; + } + /* ---GIPHY's--- */ + else if (type == 'giphy') { + downloadUrl = attachment.thumbUrl; + fileName = '${attachment.title}.gif'; + } + /* ---FILES AND VIDEOS--- */ + else if (type == 'file' || type == 'video') { + downloadUrl = attachment.assetUrl; + fileName = attachment.title; + } + + assert( + downloadUrl != null, + 'Attachment must have an assetUrl or imageUrl or thumbUrl', + ); + + final response = await Dio().get>( + downloadUrl!, + onReceiveProgress: onReceiveProgress, + queryParameters: queryParameters, + cancelToken: cancelToken, + // set responseType to `bytes` + options: options?.copyWith(responseType: ResponseType.bytes) ?? + Options(responseType: ResponseType.bytes), + ); + + final appDir = await getTemporaryDirectory(); + final ext = Uri.parse(downloadUrl).pathSegments.last; + final path = '${appDir.path}/${attachment.id}.$ext'; + + // Create an XFile for proper file saving + final file = XFile.fromData( + Uint8List.fromList(response.data!), + mimeType: attachment.mimeType, + name: fileName, + path: path, + ); + + // Save the file to the user's selected path. + await file.saveTo(path); + return path; + } +} diff --git a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart index 8d0d3f37..5d2cc40a 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart @@ -1,49 +1,44 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; -import 'package:stream_chat_flutter/src/attachment/attachment_title.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/utils/utils.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@macro image_attachment} -@Deprecated("use 'StreamImageAttachment' instead") -typedef ImageAttachment = StreamImageAttachment; - -/// {@template image_attachment} -/// Widget for showing an image attachment +/// {@template streamImageAttachment} +/// Shows an image attachment in a [StreamMessageWidget]. /// {@endtemplate} class StreamImageAttachment extends StreamAttachmentWidget { - /// Constructor for creating a [StreamImageAttachment] widget + /// {@macro streamImageAttachment} const StreamImageAttachment({ super.key, required super.message, required super.attachment, required this.messageTheme, - super.size, + super.constraints, this.showTitle = false, this.onShowMessage, - this.onReturnAction, + this.onReplyMessage, this.onAttachmentTap, this.imageThumbnailSize = const Size(400, 400), - this.imageThumbnailResizeType = 'crop', + this.imageThumbnailResizeType = 'clip', this.imageThumbnailCropType = 'center', }); - /// [StreamMessageThemeData] for showing image title + /// The [StreamMessageThemeData] to use for the image title final StreamMessageThemeData messageTheme; - /// Flag for showing title + /// Flag for whether the title should be shown or not final bool showTitle; - /// Callback when show message is tapped + /// {@macro showMessageCallback} final ShowMessageCallback? onShowMessage; - /// Callback when attachment is returned to from other screens - final ValueChanged? onReturnAction; + /// {@macro replyMessageCallback} + final ReplyMessageCallback? onReplyMessage; - /// Callback when attachment is tapped - final VoidCallback? onAttachmentTap; + /// {@macro onAttachmentTap} + final OnAttachmentTap? onAttachmentTap; /// Size of the attachment image thumbnail. final Size imageThumbnailSize; @@ -59,118 +54,126 @@ class StreamImageAttachment extends StreamAttachmentWidget { final String /*center|top|bottom|left|right*/ imageThumbnailCropType; @override - Widget build(BuildContext context) => source.when( - local: () { - if (attachment.localUri == null || attachment.file?.bytes == null) { - return AttachmentError(size: size); - } - return _buildImageAttachment( - context, - Image.memory( - attachment.file!.bytes!, - height: size?.height, - width: size?.width, - fit: BoxFit.cover, - errorBuilder: (context, _, __) => Image.asset( + Widget build(BuildContext context) { + return source.when( + local: () { + if (attachment.localUri == null || attachment.file?.bytes == null) { + return AttachmentError(constraints: constraints); + } + return _buildImageAttachment( + context, + Image.memory( + attachment.file!.bytes!, + height: constraints?.maxHeight, + width: constraints?.maxWidth, + fit: BoxFit.cover, + errorBuilder: (context, _, __) => Image.asset( + 'images/placeholder.png', + package: 'stream_chat_flutter', + ), + ), + ); + }, + network: () { + var imageUrl = + attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl; + + if (imageUrl == null) { + return AttachmentError(constraints: constraints); + } + + imageUrl = imageUrl.getResizedImageUrl( + width: imageThumbnailSize.width, + height: imageThumbnailSize.height, + resize: imageThumbnailResizeType, + crop: imageThumbnailCropType, + ); + + return _buildImageAttachment( + context, + CachedNetworkImage( + imageUrl: imageUrl, + height: constraints?.maxHeight, + width: constraints?.maxWidth, + fit: BoxFit.cover, + placeholder: (context, __) { + final image = Image.asset( 'images/placeholder.png', + fit: BoxFit.cover, package: 'stream_chat_flutter', - ), - ), - ); - }, - network: () { - var imageUrl = - attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl; + ); + final colorTheme = StreamChatTheme.of(context).colorTheme; + return Shimmer.fromColors( + baseColor: colorTheme.disabled, + highlightColor: colorTheme.inputBg, + child: image, + ); + }, + errorWidget: (context, url, error) => + AttachmentError(constraints: constraints), + ), + ); + }, + ); + } - if (imageUrl == null) return AttachmentError(size: size); - - imageUrl = imageUrl.getResizedImageUrl( - width: imageThumbnailSize.width, - height: imageThumbnailSize.height, - resize: imageThumbnailResizeType, - crop: imageThumbnailCropType, - ); - - return _buildImageAttachment( - context, - CachedNetworkImage( - imageUrl: imageUrl, - height: size?.height, - width: size?.width, - fit: BoxFit.cover, - placeholder: (context, __) { - final image = Image.asset( - 'images/placeholder.png', - fit: BoxFit.cover, - package: 'stream_chat_flutter', - ); - final colorTheme = StreamChatTheme.of(context).colorTheme; - return Shimmer.fromColors( - baseColor: colorTheme.disabled, - highlightColor: colorTheme.inputBg, - child: image, - ); - }, - errorWidget: (context, url, error) => AttachmentError(size: size), - ), - ); - }, - ); - - Widget _buildImageAttachment(BuildContext context, Widget imageWidget) => - ConstrainedBox( - constraints: BoxConstraints.loose(size!), - child: Column( - children: [ - Expanded( - child: Stack( - children: [ - GestureDetector( + Widget _buildImageAttachment(BuildContext context, Widget imageWidget) { + return Container( + constraints: constraints, + child: Column( + children: [ + Expanded( + child: Stack( + children: [ + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( onTap: onAttachmentTap ?? - () async { - final result = await Navigator.push( - context, + () { + Navigator.of(context).push( MaterialPageRoute( builder: (_) { final channel = StreamChannel.of(context).channel; return StreamChannel( channel: channel, - child: StreamFullScreenMedia( + child: StreamFullScreenMediaBuilder( mediaAttachmentPackages: message.getAttachmentPackageList(), startIndex: message.attachments.indexOf(attachment), - userName: message.user?.name, + userName: message.user!.name, onShowMessage: onShowMessage, + onReplyMessage: onReplyMessage, ), ); }, ), ); - if (result != null) onReturnAction?.call(result); }, child: imageWidget, ), - Padding( - padding: const EdgeInsets.all(8), - child: StreamAttachmentUploadStateBuilder( - message: message, - attachment: attachment, - ), + ), + Padding( + padding: const EdgeInsets.all(8), + child: StreamAttachmentUploadStateBuilder( + message: message, + attachment: attachment, ), - ], + ), + ], + ), + ), + if (showTitle && attachment.title != null) + Material( + color: messageTheme.messageBackgroundColor, + child: StreamAttachmentTitle( + messageTheme: messageTheme, + attachment: attachment, ), ), - if (showTitle && attachment.title != null) - Material( - color: messageTheme.messageBackgroundColor, - child: StreamAttachmentTitle( - messageTheme: messageTheme, - attachment: attachment, - ), - ), - ], - ), - ); + ], + ), + ); + } } diff --git a/packages/stream_chat_flutter/lib/src/attachment/image_group.dart b/packages/stream_chat_flutter/lib/src/attachment/image_group.dart new file mode 100644 index 00000000..6b7859fd --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/attachment/image_group.dart @@ -0,0 +1,175 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template streamImageGroup} +/// Constructs a group of image attachments in a [StreamMessageWidget]. +/// {@endtemplate} +class StreamImageGroup extends StatelessWidget { + /// {@macro streamImageGroup} + const StreamImageGroup({ + super.key, + required this.images, + required this.message, + required this.messageTheme, + required this.constraints, + this.onShowMessage, + this.onReplyMessage, + this.onAttachmentTap, + this.imageThumbnailSize = const Size(400, 400), + this.imageThumbnailResizeType = 'clip', + this.imageThumbnailCropType = 'center', + }); + + /// List of attachments to show + final List images; + + /// {@macro onImageGroupAttachmentTap} + final OnImageGroupAttachmentTap? onAttachmentTap; + + /// The [Message] that the images are attached to + final Message message; + + /// The [StreamMessageThemeData] to apply to this [message] + final StreamMessageThemeData messageTheme; + + /// The constraints of the [images] + final BoxConstraints constraints; + + /// {@macro showMessageCallback} + final ShowMessageCallback? onShowMessage; + + /// {@macro replyMessageCallback} + final ReplyMessageCallback? onReplyMessage; + + /// Size of the attachment image thumbnail. + final Size imageThumbnailSize; + + /// Resize type of the image attachment thumbnail. + /// + /// Defaults to [crop] + final String /*clip|crop|scale|fill*/ imageThumbnailResizeType; + + /// Crop type of the image attachment thumbnail. + /// + /// Defaults to [center] + final String /*center|top|bottom|left|right*/ imageThumbnailCropType; + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: constraints, + child: Flex( + direction: Axis.vertical, + children: [ + Flexible( + fit: FlexFit.tight, + child: Flex( + crossAxisAlignment: CrossAxisAlignment.stretch, + direction: Axis.horizontal, + children: [ + Flexible( + fit: FlexFit.tight, + child: _buildImage(context, 0), + ), + Flexible( + fit: FlexFit.tight, + child: Padding( + padding: const EdgeInsets.only(left: 2), + child: _buildImage(context, 1), + ), + ), + ], + ), + ), + if (images.length >= 3) + Flexible( + fit: FlexFit.tight, + child: Padding( + padding: const EdgeInsets.only(top: 2), + child: Flex( + direction: Axis.horizontal, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Flexible( + fit: FlexFit.tight, + child: _buildImage(context, 2), + ), + if (images.length >= 4) + Flexible( + fit: FlexFit.tight, + child: Padding( + padding: const EdgeInsets.only(left: 2), + child: Stack( + fit: StackFit.expand, + children: [ + _buildImage(context, 3), + if (images.length > 4) + Positioned.fill( + child: GestureDetector( + onTap: () => _onTap(context, 3), + child: Material( + color: Colors.black38, + child: Center( + child: Text( + '+ ${images.length - 4}', + style: const TextStyle( + color: Colors.white, + fontSize: 26, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + Future _onTap( + BuildContext context, + int index, + ) async { + if (onAttachmentTap != null) { + return onAttachmentTap!(message, images[index]); + } + + final channel = StreamChannel.of(context).channel; + + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: StreamFullScreenMediaBuilder( + mediaAttachmentPackages: message.getAttachmentPackageList(), + startIndex: index, + userName: message.user!.name, + onShowMessage: onShowMessage, + onReplyMessage: onReplyMessage, + ), + ), + ), + ); + } + + Widget _buildImage(BuildContext context, int index) { + return StreamImageAttachment( + attachment: images[index], + constraints: constraints, + message: message, + messageTheme: messageTheme, + onAttachmentTap: () => _onTap(context, index), + imageThumbnailSize: imageThumbnailSize, + imageThumbnailResizeType: imageThumbnailResizeType, + imageThumbnailCropType: imageThumbnailCropType, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/stream_attachment_package.dart b/packages/stream_chat_flutter/lib/src/attachment/stream_attachment_package.dart similarity index 89% rename from packages/stream_chat_flutter/lib/src/stream_attachment_package.dart rename to packages/stream_chat_flutter/lib/src/attachment/stream_attachment_package.dart index 0ede5142..e1e0df0e 100644 --- a/packages/stream_chat_flutter/lib/src/stream_attachment_package.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/stream_attachment_package.dart @@ -13,6 +13,6 @@ class StreamAttachmentPackage { final Attachment attachment; /// This is the message that the attachment belongs to - /// The message object may have attachemnt(s) other than the one packaged + /// The message object may have attachment(s) other than the one packaged final Message message; } diff --git a/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart index 68500d82..ce166195 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart @@ -2,15 +2,11 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@macro url_attachment} -@Deprecated("Use 'StreamUrlAttachment' instead") -typedef UrlAttachment = StreamUrlAttachment; - -/// {@template url_attachment} -/// Widget to display URL attachment +/// {@template streamUrlAttachment} +/// Displays a URL attachment in a [StreamMessageWidget]. /// {@endtemplate} class StreamUrlAttachment extends StatelessWidget { - /// Constructor for creating a [StreamUrlAttachment] + /// {@macro streamUrlAttachment} const StreamUrlAttachment({ super.key, required this.urlAttachment, @@ -32,7 +28,7 @@ class StreamUrlAttachment extends StatelessWidget { /// Padding for text final EdgeInsets textPadding; - /// [StreamMessageThemeData] for showing image title + /// The [StreamMessageThemeData] to use for the image title final StreamMessageThemeData messageTheme; /// The function called when tapping on a link @@ -41,83 +37,93 @@ class StreamUrlAttachment extends StatelessWidget { @override Widget build(BuildContext context) { final chatThemeData = StreamChatTheme.of(context); - return GestureDetector( - onTap: () { - final ogScrapeUrl = urlAttachment.ogScrapeUrl; - if (ogScrapeUrl != null) { - onLinkTap != null - ? onLinkTap!(ogScrapeUrl) - : launchURL(context, ogScrapeUrl); - } - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (urlAttachment.imageUrl != null) - Container( - clipBehavior: Clip.hardEdge, - margin: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - ), - child: Stack( - children: [ - CachedNetworkImage( - width: double.infinity, - imageUrl: urlAttachment.imageUrl!, - fit: BoxFit.cover, + + return ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400, + minWidth: 400, + ), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + final ogScrapeUrl = urlAttachment.ogScrapeUrl; + if (ogScrapeUrl != null) { + onLinkTap != null + ? onLinkTap!(ogScrapeUrl) + : launchURL(context, ogScrapeUrl); + } + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (urlAttachment.imageUrl != null) + Container( + clipBehavior: Clip.hardEdge, + margin: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), ), - Positioned( - left: 0, - bottom: -1, - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topRight: Radius.circular(16), - ), - color: messageTheme.linkBackgroundColor, + child: Stack( + children: [ + CachedNetworkImage( + width: double.infinity, + imageUrl: urlAttachment.imageUrl!, + fit: BoxFit.cover, ), - child: Padding( - padding: const EdgeInsets.only( - top: 8, - left: 8, - right: 8, - ), - child: Text( - hostDisplayName, - style: chatThemeData.textTheme.bodyBold.copyWith( - color: chatThemeData.colorTheme.accentPrimary, + Positioned( + left: 0, + bottom: -1, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topRight: Radius.circular(16), + ), + color: messageTheme.linkBackgroundColor, + ), + child: Padding( + padding: const EdgeInsets.only( + top: 8, + left: 8, + right: 8, + ), + child: Text( + hostDisplayName, + style: chatThemeData.textTheme.bodyBold.copyWith( + color: chatThemeData.colorTheme.accentPrimary, + ), + ), ), ), ), - ), + ], ), - ], + ), + Padding( + padding: textPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (urlAttachment.title != null) + Text( + urlAttachment.title!.trim(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: chatThemeData.textTheme.body + .copyWith(fontWeight: FontWeight.w700), + ), + if (urlAttachment.text != null) + Text( + urlAttachment.text!, + style: chatThemeData.textTheme.body + .copyWith(fontWeight: FontWeight.w400), + ), + ], + ), ), - ), - Padding( - padding: textPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (urlAttachment.title != null) - Text( - urlAttachment.title!.trim(), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: chatThemeData.textTheme.body - .copyWith(fontWeight: FontWeight.w700), - ), - if (urlAttachment.text != null) - Text( - urlAttachment.text!, - style: chatThemeData.textTheme.body - .copyWith(fontWeight: FontWeight.w400), - ), - ], - ), + ], ), - ], + ), ), ); } diff --git a/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart index 31f236e1..4ac9005c 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart @@ -1,135 +1,124 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/attachment/attachment_title.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart'; -import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; +import 'package:stream_chat_flutter/src/video/video_thumbnail_image.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@macro video_attachment} -@Deprecated("Use 'StreamVideoAttachment' instead") -typedef VideoAttachment = StreamVideoAttachment; - -/// {@template video_attachment} -/// Widget for showing a video attachment +/// {@template streamVideoAttachment} +/// Shows a video attachment in a [StreamMessageWidget]. /// {@endtemplate} class StreamVideoAttachment extends StreamAttachmentWidget { - /// Constructor for creating a [StreamVideoAttachment] widget + /// {@macro streamVideoAttachment} const StreamVideoAttachment({ super.key, required super.message, required super.attachment, required this.messageTheme, - super.size, + super.constraints, this.onShowMessage, - this.onReturnAction, + this.onReplyMessage, this.onAttachmentTap, }); - /// [StreamMessageThemeData] for showing title + /// The [StreamMessageThemeData] to use for the title final StreamMessageThemeData messageTheme; - /// Callback when show message is tapped + /// {@macro showMessageCallback} final ShowMessageCallback? onShowMessage; - /// Callback when attachment is returned to from other screens - final ValueChanged? onReturnAction; + /// {@macro replyMessageCallback} + final ReplyMessageCallback? onReplyMessage; - /// Callback when attachment is tapped - final VoidCallback? onAttachmentTap; + /// {@macro onAttachmentTap} + final OnAttachmentTap? onAttachmentTap; @override - Widget build(BuildContext context) => source.when( - local: () { - if (attachment.file == null) { - return AttachmentError(size: size); - } - return _buildVideoAttachment( - context, - StreamVideoThumbnailImage( - video: attachment.file!.path!, - height: size?.height, - width: size?.width, - fit: BoxFit.cover, - errorBuilder: (_, __) => AttachmentError(size: size), - ), - ); - }, - network: () { - if (attachment.assetUrl == null) { - return AttachmentError(size: size); - } - return _buildVideoAttachment( - context, - StreamVideoThumbnailImage( - video: attachment.assetUrl!, - height: size?.height, - width: size?.width, - fit: BoxFit.cover, - errorBuilder: (_, __) => AttachmentError(size: size), - ), - ); - }, - ); + Widget build(BuildContext context) { + return source.when( + local: () { + if (attachment.file == null) { + return AttachmentError(constraints: constraints); + } + return _buildVideoAttachment( + context, + StreamVideoThumbnailImage( + video: attachment.file!.path!, + constraints: constraints, + fit: BoxFit.cover, + errorBuilder: (_, __) => AttachmentError(constraints: constraints), + ), + ); + }, + network: () { + if (attachment.assetUrl == null) { + return AttachmentError(constraints: constraints); + } + return _buildVideoAttachment( + context, + StreamVideoThumbnailImage( + video: attachment.assetUrl!, + constraints: constraints, + fit: BoxFit.cover, + errorBuilder: (_, __) => AttachmentError(constraints: constraints), + ), + ); + }, + ); + } - Widget _buildVideoAttachment(BuildContext context, Widget videoWidget) => - ConstrainedBox( - constraints: BoxConstraints.loose(size ?? Size.infinite), - child: Column( - children: [ - Expanded( - child: GestureDetector( - onTap: onAttachmentTap ?? - () async { + Widget _buildVideoAttachment(BuildContext context, Widget videoWidget) { + return ConstrainedBox( + constraints: constraints ?? const BoxConstraints.expand(), + child: Column( + children: [ + Expanded( + child: GestureDetector( + onTap: onAttachmentTap ?? + () async { + if (attachment.uploadState == const UploadState.success()) { final channel = StreamChannel.of(context).channel; - final res = await Navigator.push( - context, + await Navigator.of(context).push( MaterialPageRoute( builder: (_) => StreamChannel( channel: channel, - child: StreamFullScreenMedia( + child: StreamFullScreenMediaBuilder( mediaAttachmentPackages: message.getAttachmentPackageList(), startIndex: message.attachments.indexOf(attachment), - userName: message.user?.name, + userName: message.user!.name, onShowMessage: onShowMessage, + onReplyMessage: onReplyMessage, ), ), ), ); - if (res != null) onReturnAction?.call(res); - }, - child: Stack( - children: [ - videoWidget, - const Center( - child: Material( - shape: CircleBorder(), - child: Padding( - padding: EdgeInsets.all(16), - child: Icon(Icons.play_arrow), - ), + } + }, + child: Stack( + children: [ + videoWidget, + const Center( + child: Material( + shape: CircleBorder(), + child: Padding( + padding: EdgeInsets.all(16), + child: Icon(Icons.play_arrow), ), ), - Padding( - padding: const EdgeInsets.all(8), - child: StreamAttachmentUploadStateBuilder( - message: message, - attachment: attachment, - ), + ), + Padding( + padding: const EdgeInsets.all(8), + child: StreamAttachmentUploadStateBuilder( + message: message, + attachment: attachment, ), - ], - ), + ), + ], ), ), - if (attachment.title != null) - Material( - color: messageTheme.messageBackgroundColor, - child: StreamAttachmentTitle( - messageTheme: messageTheme, - attachment: attachment, - ), - ), - ], - ), - ); + ), + ], + ), + ); + } } diff --git a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart b/packages/stream_chat_flutter/lib/src/attachment_actions_modal/attachment_actions_modal.dart similarity index 71% rename from packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart rename to packages/stream_chat_flutter/lib/src/attachment_actions_modal/attachment_actions_modal.dart index 5c2fdb17..8fb78371 100644 --- a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/attachment_actions_modal/attachment_actions_modal.dart @@ -1,20 +1,7 @@ -import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; -import 'package:image_gallery_saver/image_gallery_saver.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/utils/utils.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// Callback to download an attachment asset -typedef AttachmentDownloader = Future Function( - Attachment attachment, { - ProgressCallback? progressCallback, - DownloadedPathCallback? downloadedPathCallback, -}); - -/// Callback to receive the path once the attachment asset is downloaded -typedef DownloadedPathCallback = void Function(String? path); - /// Widget that shows the options in the gallery view class AttachmentActionsModal extends StatelessWidget { /// Returns a new [AttachmentActionsModal] @@ -23,8 +10,8 @@ class AttachmentActionsModal extends StatelessWidget { required this.attachment, required this.message, this.onShowMessage, - this.imageDownloader, - this.fileDownloader, + this.onReply, + this.attachmentDownloader, this.showReply = true, this.showShowInChat = true, this.showSave = true, @@ -41,11 +28,11 @@ class AttachmentActionsModal extends StatelessWidget { /// Callback to show the message final VoidCallback? onShowMessage; - /// Callback to download images - final AttachmentDownloader? imageDownloader; + /// Callback to reply the message + final VoidCallback? onReply; - /// Callback to provide download files - final AttachmentDownloader? fileDownloader; + /// Callback to download [attachment]. + final AttachmentDownloader? attachmentDownloader; /// Show reply option final bool showReply; @@ -69,34 +56,35 @@ class AttachmentActionsModal extends StatelessWidget { Attachment? attachment, Message? message, VoidCallback? onShowMessage, - AttachmentDownloader? imageDownloader, - AttachmentDownloader? fileDownloader, + AttachmentDownloader? attachmentDownloader, bool? showReply, bool? showShowInChat, bool? showSave, bool? showDelete, List? customActions, - }) => - AttachmentActionsModal( - key: key ?? this.key, - attachment: attachment ?? this.attachment, - message: message ?? this.message, - onShowMessage: onShowMessage ?? this.onShowMessage, - imageDownloader: imageDownloader ?? this.imageDownloader, - fileDownloader: fileDownloader ?? this.fileDownloader, - showReply: showReply ?? this.showReply, - showShowInChat: showShowInChat ?? this.showShowInChat, - showSave: showSave ?? this.showSave, - showDelete: showDelete ?? this.showDelete, - customActions: customActions ?? this.customActions, - ); + }) { + return AttachmentActionsModal( + key: key ?? this.key, + attachment: attachment ?? this.attachment, + message: message ?? this.message, + onShowMessage: onShowMessage ?? this.onShowMessage, + attachmentDownloader: attachmentDownloader ?? this.attachmentDownloader, + showReply: showReply ?? this.showReply, + showShowInChat: showShowInChat ?? this.showShowInChat, + showSave: showSave ?? this.showSave, + showDelete: showDelete ?? this.showDelete, + customActions: customActions ?? this.customActions, + ); + } @override - Widget build(BuildContext context) => GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () => Navigator.maybePop(context), - child: _buildPage(context), - ); + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => Navigator.of(context).maybePop(), + child: _buildPage(context), + ); + } Widget _buildPage(BuildContext context) { final theme = StreamChatTheme.of(context); @@ -125,9 +113,7 @@ class AttachmentActionsModal extends StatelessWidget { size: 24, color: theme.colorTheme.textLowEmphasis, ), - () { - Navigator.pop(context, ReturnActionType.reply); - }, + () => Navigator.of(context).pop(ReturnActionType.reply), ), if (showShowInChat) _buildButton( @@ -150,46 +136,44 @@ class AttachmentActionsModal extends StatelessWidget { color: theme.colorTheme.textLowEmphasis, ), () { - final isImage = attachment.type == 'image'; - final Future Function( - Attachment, { - void Function(int, int) progressCallback, - DownloadedPathCallback downloadedPathCallback, - }) saveFile = fileDownloader ?? _downloadAttachment; - final Future Function( - Attachment, { - void Function(int, int) progressCallback, - DownloadedPathCallback downloadedPathCallback, - }) saveImage = imageDownloader ?? _downloadAttachment; - final downloader = isImage ? saveImage : saveFile; + // Closing attachment actions modal before opening + // attachment download dialog + Navigator.of(context).pop(); + + final downloader = attachmentDownloader ?? + StreamAttachmentHandler.instance.downloadAttachment; + + // No need to show progress dialog in case of + // web or desktop. + if (isDesktopDeviceOrWeb) { + downloader(attachment); + return; + } final progressNotifier = ValueNotifier<_DownloadProgress?>( _DownloadProgress.initial(), ); - final downloadedPathNotifier = ValueNotifier( - null, - ); + + final downloadedPathNotifier = + ValueNotifier(null); downloader( attachment, - progressCallback: (received, total) { + onReceiveProgress: (received, total) { progressNotifier.value = _DownloadProgress( total, received, ); }, - downloadedPathCallback: (String? path) { - downloadedPathNotifier.value = path; - }, - ).catchError((e, stk) { + ).then((path) { + downloadedPathNotifier.value = path; + }).catchError((e, stk) { + print(e); + print(stk); progressNotifier.value = null; }); - // Closing attachment actions modal before opening - // attachment download dialog - Navigator.pop(context); - showDialog( barrierDismissible: false, context: context, @@ -273,30 +257,31 @@ class AttachmentActionsModal extends StatelessWidget { VoidCallback? onTap, { Color? color, Key? key, - }) => - Material( - key: key, - color: StreamChatTheme.of(context).colorTheme.barsBg, - child: InkWell( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - child: Row( - children: [ - icon, - const SizedBox(width: 16), - Text( - title, - style: StreamChatTheme.of(context) - .textTheme - .body - .copyWith(color: color), - ), - ], - ), + }) { + return Material( + key: key, + color: StreamChatTheme.of(context).colorTheme.barsBg, + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Row( + children: [ + icon, + const SizedBox(width: 16), + Text( + title, + style: StreamChatTheme.of(context) + .textTheme + .body + .copyWith(color: color), + ), + ], ), ), - ); + ), + ); + } Widget _buildDownloadProgressDialog( BuildContext context, @@ -385,29 +370,6 @@ class AttachmentActionsModal extends StatelessWidget { }, ); } - - Future _downloadAttachment( - Attachment attachment, { - ProgressCallback? progressCallback, - DownloadedPathCallback? downloadedPathCallback, - }) async { - String? filePath; - final appDocDir = await getTemporaryDirectory(); - final url = - attachment.assetUrl ?? attachment.imageUrl ?? attachment.thumbUrl!; - await Dio().download( - url, - (Headers responseHeaders) { - final ext = Uri.parse(url).pathSegments.last; - filePath ??= '${appDocDir.path}/${attachment.id}.$ext'; - return filePath!; - }, - onReceiveProgress: progressCallback, - ); - final result = await ImageGallerySaver.saveFile(filePath!); - downloadedPathCallback?.call((result as Map)['filePath'] ?? filePath); - return (result as Map)['filePath']; - } } class _DownloadProgress { @@ -426,9 +388,11 @@ class _DownloadProgress { int get toPercentage => (received * 100) ~/ total; } -/// Class for custom attachment action +/// {@template attachmentAction} +/// Defines a custom attachment action. +/// {@endtemplate} class AttachmentAction { - /// Constructor for custom attachment action + /// {@macro attachmentAction} AttachmentAction({ required this.actionTitle, required this.icon, diff --git a/packages/stream_chat_flutter/lib/src/autocomplete/stream_autocomplete.dart b/packages/stream_chat_flutter/lib/src/autocomplete/stream_autocomplete.dart new file mode 100644 index 00000000..ea9338d0 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/autocomplete/stream_autocomplete.dart @@ -0,0 +1,642 @@ +// ignore_for_file: no-empty-block + +import 'package:flutter/material.dart'; +import 'package:flutter_portal/flutter_portal.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +export 'stream_command_autocomplete_options.dart'; +export 'stream_mention_autocomplete_options.dart'; + +/// {@macro stream_chat_flutter.StreamMessageInputController} +typedef StreamMessageEditingController = StreamMessageInputController; + +/// Positions the [AutocompleteTrigger] options around the [TextField] or +/// [TextFormField] that triggered the autocomplete. +enum OptionsAlignment { + /// The options are displayed below the field. + below, + + /// The options are displayed above the field. + /// + /// This is the default. + above; + + Anchor _toAnchor() { + switch (this) { + case OptionsAlignment.below: + return const Aligned( + widthFactor: 1, + follower: Alignment.topCenter, + target: Alignment.bottomCenter, + ); + case OptionsAlignment.above: + return const Aligned( + widthFactor: 1, + follower: Alignment.bottomCenter, + target: Alignment.topCenter, + ); + } + } +} + +/// The type of the Autocomplete callback which returns the widget that +/// contains the input [TextField] or [TextFormField]. +/// +/// See also: +/// +/// * [StreamAutocomplete.fieldViewBuilder], which is of this type. +typedef StreamAutocompleteFieldViewBuilder = Widget Function( + BuildContext context, + StreamMessageEditingController messageEditingController, + FocusNode focusNode, +); + +/// The type of the [StreamAutocompleteTrigger] callback which returns a +/// [Widget] that displays the specified [options]. +/// +/// See also: +/// +/// * [StreamAutocompleteTrigger.optionsViewBuilder], which is of this type. +typedef StreamAutocompleteOptionsViewBuilder = Widget Function( + BuildContext context, + StreamAutocompleteQuery autocompleteQuery, + StreamMessageEditingController messageEditingController, +); + +/// The query to determine the autocomplete options. +class StreamAutocompleteQuery { + /// Creates a [StreamAutocompleteQuery] with the specified [query] and + /// [selection]. + const StreamAutocompleteQuery({ + required this.query, + required this.selection, + }); + + /// The query string. + final String query; + + /// The selection in the text field. + final TextSelection selection; +} + +class _StreamAutocompleteInvokedTriggerWithQuery { + const _StreamAutocompleteInvokedTriggerWithQuery(this.trigger, this.query); + + final StreamAutocompleteTrigger trigger; + final StreamAutocompleteQuery query; +} + +/// The class responsible for triggering autocomplete suggestions and +/// displaying the options. +class StreamAutocompleteTrigger { + /// Creates a [StreamAutocompleteTrigger] which can be used to trigger + /// autocomplete suggestions. + const StreamAutocompleteTrigger({ + required this.trigger, + required this.optionsViewBuilder, + this.triggerOnlyAfterSpace = false, + this.triggerOnlyAtStart = false, + this.minimumRequiredCharacters = 0, + }); + + /// The trigger character. + /// + /// eg. '@', '#', ':' + final String trigger; + + /// Whether the [trigger] should only be recognised at the start of the input. + final bool triggerOnlyAtStart; + + /// Whether the [trigger] should only be recognised after a space. + final bool triggerOnlyAfterSpace; + + /// The minimum required characters for the [trigger] to start recognising + /// a autocomplete options. + final int minimumRequiredCharacters; + + /// Builds the widget responsible for querying and displaying the + /// autocomplete options. + /// + /// See also: + /// * [StreamAutocompleteOptions], which helps in displaying the options. + final StreamAutocompleteOptionsViewBuilder optionsViewBuilder; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is StreamAutocompleteTrigger && + runtimeType == other.runtimeType && + trigger == other.trigger && + triggerOnlyAtStart == other.triggerOnlyAtStart && + triggerOnlyAfterSpace == other.triggerOnlyAfterSpace && + minimumRequiredCharacters == other.minimumRequiredCharacters; + + @override + int get hashCode => + trigger.hashCode ^ + triggerOnlyAtStart.hashCode ^ + triggerOnlyAfterSpace.hashCode ^ + minimumRequiredCharacters.hashCode; + + /// Checks if the user is invoking the recognising [trigger] and returns + /// the autocomplete query if so. + StreamAutocompleteQuery? invokingTrigger( + Message message, + TextEditingValue textEditingValue, + ) { + final text = textEditingValue.text; + final cursorPosition = textEditingValue.selection.baseOffset; + + // Find the first [trigger] location before the input cursor. + final firstTriggerIndexBeforeCursor = + text.substring(0, cursorPosition).lastIndexOf(trigger); + + // If the [trigger] is not found before the cursor, then it's not a trigger. + if (firstTriggerIndexBeforeCursor == -1) return null; + + // If the [trigger] is found before the cursor, but the [trigger] is only + // recognised at the start of the input, then it's not a trigger. + if (triggerOnlyAtStart && firstTriggerIndexBeforeCursor != 0) { + return null; + } + + // Only show typing suggestions after a space, or at the start of the input + // valid examples: "@user", "Hello @user" + // invalid examples: "Hello@user" + final textBeforeTrigger = text.substring(0, firstTriggerIndexBeforeCursor); + if (triggerOnlyAfterSpace && + textBeforeTrigger.isNotEmpty && + !textBeforeTrigger.endsWith(' ')) { + return null; + } + + // The suggestion range. Protect against invalid ranges. + final suggestionStart = firstTriggerIndexBeforeCursor + trigger.length; + final suggestionEnd = cursorPosition; + if (suggestionStart > suggestionEnd) return null; + + // Fetch the suggestion text. The suggestions can't have spaces. + // valid example: "@luke_skywa..." + // invalid example: "@luke skywa..." + final suggestionText = text.substring(suggestionStart, suggestionEnd); + if (suggestionText.contains(' ')) return null; + + // A minimum number of characters can be provided to only show + // suggestions after the customer has input enough characters. + if (suggestionText.length < minimumRequiredCharacters) return null; + + return StreamAutocompleteQuery( + query: suggestionText, + selection: TextSelection( + baseOffset: suggestionStart, + extentOffset: suggestionEnd, + ), + ); + } +} + +/// A widget that provides a text field with autocomplete functionality. +class StreamAutocomplete extends StatefulWidget { + /// Create an instance of StreamAutocomplete. + /// + /// [displayStringForOption], [optionsBuilder] and [optionsViewBuilder] must + /// not be null. + const StreamAutocomplete({ + super.key, + this.focusNode, + this.messageEditingController, + required this.autocompleteTriggers, + this.fieldViewBuilder = _defaultFieldViewBuilder, + this.optionsAlignment = OptionsAlignment.above, + this.debounceDuration = const Duration(milliseconds: 300), + }) : assert((focusNode == null) == (messageEditingController == null), ''); + + /// The triggers that trigger autocomplete. + final Iterable autocompleteTriggers; + + /// Builds the field whose input is used to get the options. + /// + /// Pass the provided [StreamMessageEditingController] to the field built + /// here so that StreamAutocomplete can listen for changes. + final StreamAutocompleteFieldViewBuilder fieldViewBuilder; + + /// The [FocusNode] that is used for the text field. + /// + /// The main purpose of this parameter is to allow the use of a separate text + /// field located in another part of the widget tree instead of the text + /// field built by [fieldViewBuilder]. For example, it may be desirable to + /// place the text field in the AppBar and the options below in the main body. + /// + /// When following this pattern, [fieldViewBuilder] can return + /// `SizedBox.shrink()` so that nothing is drawn where the text field would + /// normally be. A separate text field can be created elsewhere, and a + /// FocusNode and StreamMessageEditingController can be passed both to that + /// text field and to StreamAutocomplete. + /// + /// If this parameter is not null, then [messageEditingController] must also + /// be not null. + final FocusNode? focusNode; + + /// The [StreamMessageEditingController] that is used for the text field. + /// + /// If this parameter is not null, then [focusNode] must also be not null. + final StreamMessageEditingController? messageEditingController; + + /// The alignment of the options. + /// + /// The default value is [OptionsAlignment.above]. + final OptionsAlignment optionsAlignment; + + /// The duration of the debounce period for the + /// [StreamMessageEditingController]. + /// + /// The default value is [300ms]. + final Duration debounceDuration; + + static Widget _defaultFieldViewBuilder( + BuildContext context, + StreamMessageEditingController messageEditingController, + FocusNode focusNode, + ) { + return _StreamAutocompleteField( + focusNode: focusNode, + messageEditingController: messageEditingController, + ); + } + + /// Returns the nearest [StreamAutocomplete] ancestor of the given context. + static _StreamAutocompleteState of(BuildContext context) { + final state = context.findAncestorStateOfType<_StreamAutocompleteState>(); + assert(state != null, 'StreamAutocomplete not found in the widget tree'); + return state!; + } + + @override + _StreamAutocompleteState createState() => _StreamAutocompleteState(); +} + +class _StreamAutocompleteState extends State { + late StreamMessageEditingController _messageEditingController; + late FocusNode _focusNode; + + StreamAutocompleteQuery? _currentQuery; + StreamAutocompleteTrigger? _currentTrigger; + + bool _hideOptions = false; + String _lastFieldText = ''; + + // True if the state indicates that the options should be visible. + bool get _shouldShowOptions { + return !_hideOptions && + _focusNode.hasFocus && + _currentQuery != null && + _currentTrigger != null; + } + + /// Accepts and replaces the current query with the given [option] and closes + /// the suggested options. + /// + /// Optionally, pass [keepTrigger] false to remove the trigger from the text. + void acceptAutocompleteOption( + String option, { + bool keepTrigger = true, + }) { + if (option.isEmpty) return; + + final query = _currentQuery; + final trigger = _currentTrigger; + if (query == null || trigger == null) return; + + final querySelection = query.selection; + final text = _messageEditingController.text; + + var start = querySelection.baseOffset; + if (!keepTrigger) start -= 1; + + final end = querySelection.extentOffset; + + final alreadyContainsSpace = text.substring(end).startsWith(' '); + // Having extra space helps dismissing the auto-completion view. + // ignore: parameter_assignments + if (!alreadyContainsSpace) option += ' '; + + var selectionOffset = start + option.length; + // In case the extra space is already there, we need to move the cursor + // after the space. + if (alreadyContainsSpace) selectionOffset += 1; + + final newText = text.replaceRange(start, end, option); + final newSelection = TextSelection.collapsed(offset: selectionOffset); + + _messageEditingController.textEditingValue = TextEditingValue( + text: newText, + selection: newSelection, + ); + + return closeSuggestions(); + } + + /// Closes the suggestions and resets the current query. + void closeSuggestions() { + final prev = _currentQuery; + if (prev == null) return; + + _currentQuery = null; + if (mounted) setState(() {}); + } + + /// Starts showing the suggestions for the given [query]. + void showSuggestions( + StreamAutocompleteQuery query, + StreamAutocompleteTrigger trigger, + ) { + final prevQuery = _currentQuery; + final prevTrigger = _currentTrigger; + if (prevQuery == query && prevTrigger == trigger) return; + + _currentQuery = query; + _currentTrigger = trigger; + if (mounted) setState(() {}); + } + + // Checks if there is any invoked autocomplete trigger and returns the first + // one along with the query that matches the current input. + _StreamAutocompleteInvokedTriggerWithQuery? _getInvokedTriggerWithQuery( + Message messageValue, + TextEditingValue textEditingValue, + ) { + final autocompleteTriggers = widget.autocompleteTriggers.toSet(); + for (final trigger in autocompleteTriggers) { + final query = trigger.invokingTrigger(messageValue, textEditingValue); + if (query != null) { + return _StreamAutocompleteInvokedTriggerWithQuery(trigger, query); + } + } + return null; + } + + // Called when _textEditingController changes. + late final _onChangedField = debounce( + () { + final messageValue = _messageEditingController.message; + final textEditingValue = _messageEditingController.textEditingValue; + + // If the content has not changed, then there is nothing to do. + if (textEditingValue.text == _lastFieldText) return; + + // Make sure the options are no longer hidden if the content of the + // field changes. + _hideOptions = false; + _lastFieldText = textEditingValue.text; + + // If the text field is empty, then there is no need to do anything. + if (textEditingValue.text.isEmpty) return closeSuggestions(); + + // If the text field is not empty, then we need to check if the + // text field contains a trigger. + final _triggerWithQuery = _getInvokedTriggerWithQuery( + messageValue, + textEditingValue, + ); + + // If the text field does not contain a trigger, then there is no need + // to do anything. + if (_triggerWithQuery == null) return closeSuggestions(); + + // If the text field contains a trigger, then we need to open the + // portal. + final trigger = _triggerWithQuery.trigger; + final query = _triggerWithQuery.query; + return showSuggestions(query, trigger); + }, + widget.debounceDuration, + ); + + // Called when the field's FocusNode changes. + void _onChangedFocus() { + // Options should no longer be hidden when the field is re-focused. + _hideOptions = !_focusNode.hasFocus; + if (mounted) setState(() {}); + } + + // Handle a potential change in textEditingController by properly disposing of + // the old one and setting up the new one, if needed. + void _updateTextEditingController( + StreamMessageEditingController? old, + StreamMessageEditingController? current, + ) { + if ((old == null && current == null) || old == current) { + return; + } + if (old == null) { + _messageEditingController + ..removeListener(_onChangedField) + ..dispose(); + _messageEditingController = current!; + } else if (current == null) { + _messageEditingController.removeListener(_onChangedField); + _messageEditingController = StreamMessageEditingController(); + } else { + _messageEditingController.removeListener(_onChangedField); + _messageEditingController = current; + } + _messageEditingController.addListener(_onChangedField); + } + + // Handle a potential change in focusNode by properly disposing of the old one + // and setting up the new one, if needed. + void _updateFocusNode(FocusNode? old, FocusNode? current) { + if ((old == null && current == null) || old == current) { + return; + } + if (old == null) { + _focusNode + ..removeListener(_onChangedFocus) + ..dispose(); + _focusNode = current!; + } else if (current == null) { + _focusNode.removeListener(_onChangedFocus); + _focusNode = FocusNode(); + } else { + _focusNode.removeListener(_onChangedFocus); + _focusNode = current; + } + _focusNode.addListener(_onChangedFocus); + } + + @override + void initState() { + super.initState(); + _messageEditingController = + widget.messageEditingController ?? StreamMessageEditingController(); + _messageEditingController.addListener(_onChangedField); + _focusNode = widget.focusNode ?? FocusNode(); + _focusNode.addListener(_onChangedFocus); + } + + @override + void didUpdateWidget(StreamAutocomplete oldWidget) { + super.didUpdateWidget(oldWidget); + _updateTextEditingController( + oldWidget.messageEditingController, + widget.messageEditingController, + ); + _updateFocusNode(oldWidget.focusNode, widget.focusNode); + } + + @override + void dispose() { + _messageEditingController.removeListener(_onChangedField); + if (widget.messageEditingController == null) { + _messageEditingController.dispose(); + } + _focusNode.removeListener(_onChangedFocus); + if (widget.focusNode == null) { + _focusNode.dispose(); + } + _onChangedField.cancel(); + closeSuggestions(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // Adding additional builder so that [.of] works. + return Builder( + builder: (context) { + final anchor = widget.optionsAlignment._toAnchor(); + final shouldShowOptions = _shouldShowOptions; + final optionViewBuilder = shouldShowOptions + ? _currentTrigger!.optionsViewBuilder( + context, + _currentQuery!, + _messageEditingController, + ) + : null; + + return PortalTarget( + anchor: anchor, + visible: shouldShowOptions, + portalFollower: optionViewBuilder, + child: widget.fieldViewBuilder( + context, + _messageEditingController, + _focusNode, + ), + ); + }, + ); + } +} + +// The default Material-style Autocomplete text field. +class _StreamAutocompleteField extends StatelessWidget { + const _StreamAutocompleteField({ + required this.focusNode, + required this.messageEditingController, + }); + + final FocusNode focusNode; + + final StreamMessageEditingController messageEditingController; + + @override + Widget build(BuildContext context) { + return StreamMessageTextField( + controller: messageEditingController, + focusNode: focusNode, + ); + } +} + +const _kDefaultStreamAutocompleteOptionsShape = RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8)), +); + +/// A helper widget used to show the options of a [StreamAutocomplete]. +class StreamAutocompleteOptions extends StatelessWidget { + /// Creates a [StreamAutocompleteOptions] widget. + const StreamAutocompleteOptions({ + super.key, + this.color, + this.elevation = 2, + this.margin = const EdgeInsets.all(8), + this.clipBehavior = Clip.hardEdge, + required this.options, + this.maxHeight, + required this.optionBuilder, + this.headerBuilder, + this.shape = _kDefaultStreamAutocompleteOptionsShape, + }); + + /// The background color of the options card. + /// + /// Defaults to [StreamColorTheme.barsBg]. + final Color? color; + + /// The elevation of the options card. + /// + /// The default value is 2. + final double elevation; + + /// The margin of the options card. + /// + /// The default value is [EdgeInsets.all(8)]. + final EdgeInsetsGeometry margin; + + /// The clip behavior of the options card. + /// + /// The default value is [Clip.hardEdge]. + final Clip clipBehavior; + + /// The shape of the options card. + final ShapeBorder shape; + + /// The options to display. + final Iterable options; + + /// The maximum height of the options card. + /// + /// Defaults to half the height of the screen. + final double? maxHeight; + + /// The builder for the options. + final Widget Function(BuildContext context, T option) optionBuilder; + + /// The builder for the header of the options. + final WidgetBuilder? headerBuilder; + + @override + Widget build(BuildContext context) { + final height = MediaQuery.of(context).size.height; + final colorTheme = StreamChatTheme.of(context).colorTheme; + return Card( + margin: margin, + elevation: elevation, + color: color ?? colorTheme.barsBg, + shape: shape, + clipBehavior: clipBehavior, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (headerBuilder != null) ...[ + headerBuilder!(context), + const Divider(height: 0), + ], + LimitedBox( + maxHeight: maxHeight ?? height * 0.5, + child: ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.zero, + itemCount: options.length, + itemBuilder: (context, index) { + final option = options.elementAt(index); + return optionBuilder(context, option); + }, + ), + ), + ], + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/autocomplete/stream_command_autocomplete_options.dart b/packages/stream_chat_flutter/lib/src/autocomplete/stream_command_autocomplete_options.dart new file mode 100644 index 00000000..ad4295e0 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/autocomplete/stream_command_autocomplete_options.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/autocomplete/stream_autocomplete.dart'; +import 'package:stream_chat_flutter/src/misc/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; + +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// {@template commands_overlay} +/// Overlay for displaying commands that can be used +/// to interact with the channel. +/// {@endtemplate} +class StreamCommandAutocompleteOptions extends StatelessWidget { + /// Constructor for creating a [StreamCommandAutocompleteOptions] + const StreamCommandAutocompleteOptions({ + required this.query, + required this.channel, + this.onCommandSelected, + super.key, + }); + + /// Query for searching commands. + final String query; + + /// The channel to search for users. + final Channel channel; + + /// Callback called when a command is selected. + final ValueSetter? onCommandSelected; + + @override + Widget build(BuildContext context) { + final commands = channel.config?.commands.where((it) { + final normalizedQuery = query.toUpperCase(); + final normalizedName = it.name.toUpperCase(); + return normalizedName.contains(normalizedQuery); + }); + + if (commands == null || commands.isEmpty) return const SizedBox.shrink(); + + final streamChatTheme = StreamChatTheme.of(context); + final colorTheme = streamChatTheme.colorTheme; + final textTheme = streamChatTheme.textTheme; + + return StreamAutocompleteOptions( + options: commands, + headerBuilder: (context) { + return ListTile( + dense: true, + horizontalTitleGap: 0, + leading: StreamSvgIcon.lightning( + color: colorTheme.accentPrimary, + size: 28, + ), + title: Text( + context.translations.instantCommandsLabel, + style: TextStyle( + color: colorTheme.textHighEmphasis.withOpacity(0.5), + ), + ), + ); + }, + optionBuilder: (context, command) { + return ListTile( + dense: true, + horizontalTitleGap: 0, + leading: _CommandIcon(command: command), + title: Row( + children: [ + Text( + command.name.capitalize(), + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 8), + Text( + '/${command.name} ${command.args}', + style: textTheme.body.copyWith( + color: colorTheme.textLowEmphasis, + ), + ), + ], + ), + onTap: onCommandSelected == null + ? null + : () => onCommandSelected!(command), + ); + }, + ); + } +} + +class _CommandIcon extends StatelessWidget { + const _CommandIcon({required this.command}); + + final Command command; + + @override + Widget build(BuildContext context) { + final _streamChatTheme = StreamChatTheme.of(context); + switch (command.name) { + case 'giphy': + return CircleAvatar( + radius: 12, + child: StreamSvgIcon.giphyIcon( + size: 24, + ), + ); + case 'ban': + return CircleAvatar( + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, + radius: 12, + child: StreamSvgIcon.iconUserDelete( + size: 16, + color: Colors.white, + ), + ); + case 'flag': + return CircleAvatar( + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, + radius: 12, + child: StreamSvgIcon.flag( + size: 14, + color: Colors.white, + ), + ); + case 'imgur': + return CircleAvatar( + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, + radius: 12, + child: ClipOval( + child: StreamSvgIcon.imgur( + size: 24, + ), + ), + ); + case 'mute': + return CircleAvatar( + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, + radius: 12, + child: StreamSvgIcon.mute( + size: 16, + color: Colors.white, + ), + ); + case 'unban': + return CircleAvatar( + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, + radius: 12, + child: StreamSvgIcon.userAdd( + size: 16, + color: Colors.white, + ), + ); + case 'unmute': + return CircleAvatar( + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, + radius: 12, + child: StreamSvgIcon.volumeUp( + size: 16, + color: Colors.white, + ), + ); + default: + return CircleAvatar( + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, + radius: 12, + child: StreamSvgIcon.lightning( + size: 16, + color: Colors.white, + ), + ); + } + } +} diff --git a/packages/stream_chat_flutter/lib/src/user_mentions_overlay.dart b/packages/stream_chat_flutter/lib/src/autocomplete/stream_mention_autocomplete_options.dart similarity index 60% rename from packages/stream_chat_flutter/lib/src/user_mentions_overlay.dart rename to packages/stream_chat_flutter/lib/src/autocomplete/stream_mention_autocomplete_options.dart index 6a94a476..ad50c21b 100644 --- a/packages/stream_chat_flutter/lib/src/user_mentions_overlay.dart +++ b/packages/stream_chat_flutter/lib/src/autocomplete/stream_mention_autocomplete_options.dart @@ -1,31 +1,20 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; -import 'package:stream_chat_flutter/src/user_mention_tile.dart'; +import 'package:stream_chat_flutter/src/autocomplete/stream_autocomplete.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/user/user_mention_tile.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/src/utils/typedefs.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -/// Builder function for building a mention tile. -/// -/// Use [StreamUserMentionTile] for the default implementation. -typedef MentionTileBuilder = Widget Function( - BuildContext context, - User user, -); - -/// {@macro user_mention_tile} -@Deprecated("Use 'StreamUserMentionsOverlay' instead") -typedef UserMentionsOverlay = StreamUserMentionsOverlay; - /// {@template user_mentions_overlay} /// Overlay for displaying users that can be mentioned. /// {@endtemplate} -class StreamUserMentionsOverlay extends StatefulWidget { - /// Constructor for creating a [StreamUserMentionsOverlay]. - StreamUserMentionsOverlay({ +class StreamMentionAutocompleteOptions extends StatefulWidget { + /// Constructor for creating a [StreamMentionAutocompleteOptions]. + StreamMentionAutocompleteOptions({ super.key, required this.query, required this.channel, - required this.size, this.client, this.limit = 10, this.mentionAllAppUsers = false, @@ -46,9 +35,6 @@ class StreamUserMentionsOverlay extends StatefulWidget { /// Limit applied on user search results. final int limit; - /// The size of the overlay. - final Size size; - /// The channel to search for users. final Channel channel; @@ -61,17 +47,18 @@ class StreamUserMentionsOverlay extends StatefulWidget { final bool mentionAllAppUsers; /// Customize the tile for the mentions overlay. - final MentionTileBuilder? mentionsTileBuilder; + final UserMentionTileBuilder? mentionsTileBuilder; /// Callback called when a user is selected. - final void Function(User user)? onMentionUserTap; + final ValueSetter? onMentionUserTap; @override - _StreamUserMentionsOverlayState createState() => - _StreamUserMentionsOverlayState(); + _StreamMentionAutocompleteOptionsState createState() => + _StreamMentionAutocompleteOptionsState(); } -class _StreamUserMentionsOverlayState extends State { +class _StreamMentionAutocompleteOptionsState + extends State { late Future> userMentionsFuture; @override @@ -81,7 +68,7 @@ class _StreamUserMentionsOverlayState extends State { } @override - void didUpdateWidget(covariant StreamUserMentionsOverlay oldWidget) { + void didUpdateWidget(covariant StreamMentionAutocompleteOptions oldWidget) { super.didUpdateWidget(oldWidget); if (widget.channel != oldWidget.channel || widget.query != oldWidget.query || @@ -93,43 +80,30 @@ class _StreamUserMentionsOverlayState extends State { @override Widget build(BuildContext context) { - final theme = StreamChatTheme.of(context); - return Card( - margin: const EdgeInsets.all(8), - elevation: 2, - color: theme.colorTheme.barsBg, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - clipBehavior: Clip.hardEdge, - child: Container( - constraints: BoxConstraints.loose(widget.size), - decoration: BoxDecoration(color: theme.colorTheme.barsBg), - child: FutureBuilder>( - future: userMentionsFuture, - builder: (context, snapshot) { - if (snapshot.hasError) return const Offstage(); - if (!snapshot.hasData) return const Offstage(); - final users = snapshot.data!; - return ListView.builder( - padding: EdgeInsets.zero, - shrinkWrap: true, - itemCount: users.length, - itemBuilder: (context, index) { - final user = users[index]; - return Material( - color: theme.colorTheme.barsBg, - child: InkWell( - onTap: () => widget.onMentionUserTap?.call(user), - child: widget.mentionsTileBuilder?.call(context, user) ?? - StreamUserMentionTile(user), - ), - ); - }, + return FutureBuilder>( + future: userMentionsFuture, + builder: (context, snapshot) { + if (snapshot.hasError) return const SizedBox.shrink(); + if (!snapshot.hasData) return const SizedBox.shrink(); + final users = snapshot.data!; + + return StreamAutocompleteOptions( + options: users, + optionBuilder: (context, user) { + final colorTheme = StreamChatTheme.of(context).colorTheme; + return Material( + color: colorTheme.barsBg, + child: InkWell( + onTap: widget.onMentionUserTap == null + ? null + : () => widget.onMentionUserTap!(user), + child: widget.mentionsTileBuilder?.call(context, user) ?? + StreamUserMentionTile(user), + ), ); }, - ), - ), + ); + }, ); } diff --git a/packages/stream_chat_flutter/lib/src/gradient_avatar.dart b/packages/stream_chat_flutter/lib/src/avatars/gradient_avatar.dart similarity index 86% rename from packages/stream_chat_flutter/lib/src/gradient_avatar.dart rename to packages/stream_chat_flutter/lib/src/avatars/gradient_avatar.dart index 53249ec4..4c7f23d2 100644 --- a/packages/stream_chat_flutter/lib/src/gradient_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/avatars/gradient_avatar.dart @@ -3,15 +3,11 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; -/// {@macro gradient_avatar} -@Deprecated("Use 'StreamGradientAvatar' instead") -typedef GradientAvatar = StreamGradientAvatar; - -/// {@template gradient_avatar} -/// Fallback user avatar with a polygon gradient overlayed with text +/// {@template streamGradientAvatar} +/// Fallback user avatar with a polygon gradient overlaid with text /// {@endtemplate} class StreamGradientAvatar extends StatefulWidget { - /// Constructor for [StreamGradientAvatar] + /// {@macro streamGradientAvatar} const StreamGradientAvatar({ super.key, required this.name, @@ -30,18 +26,20 @@ class StreamGradientAvatar extends StatefulWidget { class _StreamGradientAvatarState extends State { @override - Widget build(BuildContext context) => Center( - child: RepaintBoundary( - child: CustomPaint( - painter: DemoPainter( - widget.userId, - getShortenedName(widget.name), - DefaultTextStyle.of(context).style.fontFamily ?? 'Roboto', - ), - child: const SizedBox.expand(), + Widget build(BuildContext context) { + return Center( + child: RepaintBoundary( + child: CustomPaint( + painter: PolygonGradientPainter( + widget.userId, + getShortenedName(widget.name), + DefaultTextStyle.of(context).style.fontFamily ?? 'Roboto', ), + child: const SizedBox.expand(), ), - ); + ), + ); + } String getShortenedName(String name) { var parts = name.split(' ')..removeWhere((e) => e == ''); @@ -60,19 +58,21 @@ class _StreamGradientAvatarState extends State { } } +/// {@template polygonGradientPainter} /// Painter for bg polygon gradient -class DemoPainter extends CustomPainter { - /// Constructor for [DemoPainter] - DemoPainter( +/// {@endtemplate} +class PolygonGradientPainter extends CustomPainter { + /// {@macro polygonGradientPainter} + PolygonGradientPainter( this.userId, this.username, this.fontFamily, ); - /// Init grid row count + /// Initial grid row count static const int rowCount = 5; - /// Init grid column count + /// Initial grid column count static const int columnCount = 5; /// User ID used for key @@ -154,7 +154,7 @@ class DemoPainter extends CustomPainter { @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; - /// Transforms initial grid into a polygon grid + /// Transforms initial grid into a polygon grid. List transformPoints(Set points, Size size) { final transformedList = []; final orgList = points.toList(); @@ -185,9 +185,11 @@ class DemoPainter extends CustomPainter { } } -/// Class for storing and drawing four points of a polygon +/// {@template offset4} +/// Class for storing and drawing four points of a polygon. +/// {@endtemplate} class Offset4 { - /// Constructor for [Offset4] + /// {@macro offset4} Offset4( this.p1, this.p2, diff --git a/packages/stream_chat_flutter/lib/src/group_avatar.dart b/packages/stream_chat_flutter/lib/src/avatars/group_avatar.dart similarity index 92% rename from packages/stream_chat_flutter/lib/src/group_avatar.dart rename to packages/stream_chat_flutter/lib/src/avatars/group_avatar.dart index a07d60d8..0571c7fd 100644 --- a/packages/stream_chat_flutter/lib/src/group_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/avatars/group_avatar.dart @@ -1,15 +1,11 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@macro group_avatar} -@Deprecated("Use 'StreamGroupAvatar' instead") -typedef GroupAvatar = StreamGroupAvatar; - -/// {@template group_avatar} +/// {@template streamGroupAvatar} /// Widget for constructing a group of images /// {@endtemplate} class StreamGroupAvatar extends StatelessWidget { - /// Constructor for creating a [StreamGroupAvatar] + /// {@macro streamGroupAvatar} const StreamGroupAvatar({ super.key, this.channel, @@ -25,25 +21,28 @@ class StreamGroupAvatar extends StatelessWidget { /// The channel of the avatar final Channel? channel; - /// List of images to display + /// The list of members in the group whose avatars should be displayed. final List members; /// Constraints on the widget final BoxConstraints? constraints; - /// Callback when widget is tapped + /// The action to perform when the widget is tapped final VoidCallback? onTap; - /// Highlights if selected + /// If `true`, this widget should be highlighted. + /// + /// Defaults to `false`. final bool selected; /// [BorderRadius] to pass to the widget final BorderRadius? borderRadius; - /// Color of selection if selected + /// The color to highlight the widget with if [selected] is `true` final Color? selectionColor; - /// Thickness with which color of selection is shown + /// The value to use for the border thickness and padding of the + /// selected image final double selectionThickness; @override diff --git a/packages/stream_chat_flutter/lib/src/user_avatar.dart b/packages/stream_chat_flutter/lib/src/avatars/user_avatar.dart similarity index 85% rename from packages/stream_chat_flutter/lib/src/user_avatar.dart rename to packages/stream_chat_flutter/lib/src/avatars/user_avatar.dart index 406f966f..2290fca9 100644 --- a/packages/stream_chat_flutter/lib/src/user_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/avatars/user_avatar.dart @@ -2,15 +2,11 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@macro user_avatar} -@Deprecated("Use 'StreamUserAvatar' instead") -typedef UserAvatar = StreamUserAvatar; - -/// {@template user_avatar} -/// Widget that displays a user avatar +/// {@template streamUserAvatar} +/// Displays a user's avatar. /// {@endtemplate} class StreamUserAvatar extends StatelessWidget { - /// Constructor to create a [StreamUserAvatar] + /// {@macro streamUserAvatar} const StreamUserAvatar({ super.key, required this.user, @@ -27,54 +23,63 @@ class StreamUserAvatar extends StatelessWidget { this.placeholder, }); - /// User whose avatar is to displayed + /// User whose avatar is to be displayed final User user; /// Alignment of the online indicator + /// + /// Defaults to `Alignment.topRight` final Alignment onlineIndicatorAlignment; - /// Size of the avatar + /// Sizing constraints of the avatar final BoxConstraints? constraints; /// [BorderRadius] of the image final BorderRadius? borderRadius; - /// Size of the online indicator + /// Sizing constraints of the online indicator final BoxConstraints? onlineIndicatorConstraints; - /// Callback when avatar is tapped - final void Function(User)? onTap; + /// {@macro onUserAvatarTap} + final OnUserAvatarPress? onTap; - /// Callback when avatar is long pressed - final void Function(User)? onLongPress; + /// {@macro onUserAvatarTap} + final OnUserAvatarPress? onLongPress; /// Flag for showing online status + /// + /// Defaults to `true` final bool showOnlineStatus; /// Flag for if avatar is selected + /// + /// Defaults to `false` final bool selected; /// Color of selection final Color? selectionColor; /// Selection thickness around the avatar + /// + /// Defaults to `4` final double selectionThickness; - /// The widget that will be built when the user image is loading - final Widget Function(BuildContext, User)? placeholder; + /// {@macro placeholderUserImage} + final PlaceholderUserImage? placeholder; @override Widget build(BuildContext context) { final hasImage = user.image != null && user.image!.isNotEmpty; final streamChatTheme = StreamChatTheme.of(context); + final streamChatConfig = StreamChatConfiguration.of(context); final placeholder = - this.placeholder ?? streamChatTheme.placeholderUserImage; + this.placeholder ?? streamChatConfig.placeholderUserImage; final backupGradientAvatar = ClipRRect( borderRadius: borderRadius ?? streamChatTheme.ownMessageTheme.avatarTheme?.borderRadius, - child: streamChatTheme.defaultUserImage(context, user), + child: streamChatConfig.defaultUserImage(context, user), ); Widget avatar = FittedBox( diff --git a/packages/stream_chat_flutter/lib/src/back_button.dart b/packages/stream_chat_flutter/lib/src/back_button.dart deleted file mode 100644 index 6d54695d..00000000 --- a/packages/stream_chat_flutter/lib/src/back_button.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// Back button implementation -// ignore: prefer-match-file-name -class StreamBackButton extends StatelessWidget { - /// Constructor for creating back button - const StreamBackButton({ - super.key, - this.onPressed, - this.showUnreads = false, - this.cid, - }); - - /// Callback for when button is pressed - final VoidCallback? onPressed; - - /// Show unread count - final bool showUnreads; - - /// Channel cid used to retrieve unread count - final String? cid; - - @override - Widget build(BuildContext context) => Stack( - alignment: Alignment.center, - children: [ - RawMaterialButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - ), - elevation: 0, - highlightElevation: 0, - focusElevation: 0, - hoverElevation: 0, - onPressed: () { - if (onPressed != null) { - onPressed!(); - } else { - Navigator.maybePop(context); - } - }, - padding: const EdgeInsets.all(14), - child: StreamSvgIcon.left( - size: 24, - color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, - ), - ), - if (showUnreads) - Positioned( - top: 7, - right: 7, - child: StreamUnreadIndicator( - cid: cid, - ), - ), - ], - ); -} diff --git a/packages/stream_chat_flutter/lib/src/bottom_sheets/attachment_modal_sheet.dart b/packages/stream_chat_flutter/lib/src/bottom_sheets/attachment_modal_sheet.dart new file mode 100644 index 00000000..2a7b963a --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/bottom_sheets/attachment_modal_sheet.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; + +/// {@template attachmentModalSheet} +/// The modalBottomSheet that appears when a mobile user attempts to add +/// attachments to a chat. +/// +/// Should not be used on desktop or web. +/// {@endtemplate} +class AttachmentModalSheet extends StatelessWidget { + /// {@macro attachmentModalSheet} + const AttachmentModalSheet({ + super.key, + required this.onFileTap, + required this.onPhotoTap, + required this.onVideoTap, + }); + + /// The action to perform when the "file" button is tapped. + final VoidCallback onFileTap; + + /// The action to perform when the "photo" button is tapped. + final VoidCallback onPhotoTap; + + /// The action to perform when the "video" button is tapped. + final VoidCallback onVideoTap; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text( + context.translations.addAFileLabel, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + ListTile( + leading: const Icon(Icons.image), + title: Text(context.translations.uploadAPhotoLabel), + onTap: () { + onPhotoTap.call(); + Navigator.of(context).pop(); + }, + ), + ListTile( + leading: const Icon(Icons.video_library), + title: Text(context.translations.uploadAVideoLabel), + onTap: () { + onVideoTap.call(); + Navigator.of(context).pop(); + }, + ), + ListTile( + leading: const Icon(Icons.insert_drive_file), + title: Text(context.translations.uploadAFileLabel), + onTap: () { + onFileTap.call(); + Navigator.of(context).pop(); + }, + ), + ], + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/bottom_sheets/edit_message_sheet.dart b/packages/stream_chat_flutter/lib/src/bottom_sheets/edit_message_sheet.dart new file mode 100644 index 00000000..c27a0ed0 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/bottom_sheets/edit_message_sheet.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/utils.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template editMessageSheet} +/// Allows a user to edit the selected message. +/// {@endtemplate} +class EditMessageSheet extends StatefulWidget { + /// {@macro editMessageSheet} + const EditMessageSheet({ + super.key, + required this.message, + required this.channel, + this.editMessageInputBuilder, + }); + + /// {@macro editMessageInputBuilder} + final EditMessageInputBuilder? editMessageInputBuilder; + + /// The message to edit. + final Message message; + + /// The [StreamChannel] above this widget. + final Channel channel; + + @override + State createState() => _EditMessageSheetState(); +} + +class _EditMessageSheetState extends State { + late final controller = StreamMessageInputController( + message: widget.message, + ); + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final streamChatThemeData = StreamChatTheme.of(context); + return KeyboardShortcutRunner( + onEscapeKeypress: () => Navigator.of(context).pop(), + child: Padding( + padding: MediaQuery.of(context).viewInsets, + child: StreamChannel( + channel: widget.channel, + child: Flex( + direction: Axis.vertical, + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: StreamSvgIcon.edit( + color: streamChatThemeData.colorTheme.disabled, + ), + ), + Text( + context.translations.editMessageLabel, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + IconButton( + visualDensity: VisualDensity.compact, + icon: StreamSvgIcon.closeSmall(), + onPressed: Navigator.of(context).pop, + ), + ], + ), + ), + if (widget.editMessageInputBuilder != null) + widget.editMessageInputBuilder!(context, widget.message) + else + StreamMessageInput( + messageInputController: controller, + preMessageSending: (m) { + FocusScope.of(context).unfocus(); + Navigator.of(context).pop(); + return m; + }, + ), + ], + ), + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/bottom_sheets/error_alert_sheet.dart b/packages/stream_chat_flutter/lib/src/bottom_sheets/error_alert_sheet.dart new file mode 100644 index 00000000..6a9adbc2 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/bottom_sheets/error_alert_sheet.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template errorAlertSheet} +/// A bottom sheet that displays when an error occurs. +/// +/// Should only be used on mobile platforms. +/// {@endtemplate} +class ErrorAlertSheet extends StatelessWidget { + /// {@macro errorAlertSheet} + const ErrorAlertSheet({ + super.key, + required this.errorDescription, + }); + + /// The description of the error. + final String errorDescription; + + @override + Widget build(BuildContext context) { + final _streamChatTheme = StreamChatTheme.of(context); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 26, + ), + StreamSvgIcon.error( + color: _streamChatTheme.colorTheme.accentError, + size: 24, + ), + const SizedBox( + height: 26, + ), + Text( + context.translations.somethingWentWrongError, + style: _streamChatTheme.textTheme.headlineBold, + ), + const SizedBox( + height: 7, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + errorDescription, + textAlign: TextAlign.center, + ), + ), + const SizedBox( + height: 36, + ), + Container( + color: _streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.08), + height: 1, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + context.translations.okLabel, + style: _streamChatTheme.textTheme.bodyBold.copyWith( + color: _streamChatTheme.colorTheme.accentPrimary, + ), + ), + ), + ], + ), + ], + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/bottom_sheets/stream_channel_info_bottom_sheet.dart similarity index 95% rename from packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart rename to packages/stream_chat_flutter/lib/src/bottom_sheets/stream_channel_info_bottom_sheet.dart index 6827c431..e579faf3 100644 --- a/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart +++ b/packages/stream_chat_flutter/lib/src/bottom_sheets/stream_channel_info_bottom_sheet.dart @@ -1,13 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/channel_info.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/option_list_tile.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; -import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; -import 'package:stream_chat_flutter/src/theme/themes.dart'; -import 'package:stream_chat_flutter/src/user_avatar.dart'; -import 'package:stream_chat_flutter/src/v4/stream_channel_name.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// A [BottomSheet] that shows information about a [Channel]. class StreamChannelInfoBottomSheet extends StatelessWidget { diff --git a/packages/stream_chat_flutter/lib/src/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel/channel_header.dart similarity index 84% rename from packages/stream_chat_flutter/lib/src/channel_header.dart rename to packages/stream_chat_flutter/lib/src/channel/channel_header.dart index 8d9205d2..4bbe4f56 100644 --- a/packages/stream_chat_flutter/lib/src/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_header.dart @@ -1,18 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:stream_chat_flutter/src/channel_info.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -///{@macro template_name} -@Deprecated("Use 'StreamChannelHeader' instead") -typedef ChannelHeader = StreamChannelHeader; - -/// {@template channel_header} +/// {@template streamChannelHeader} /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header_paint.png) /// -/// It shows the current [Channel] information. +/// Shows information about the current [Channel]. /// /// ```dart /// class MyApp extends StatelessWidget { @@ -39,26 +34,26 @@ typedef ChannelHeader = StreamChannelHeader; /// ``` /// /// Usually you would use this widget as an [AppBar] inside a [Scaffold]. -/// However you can also use it as a normal widget. +/// However, you can also use it as a normal widget. /// /// Make sure to have a [StreamChannel] ancestor in order to provide the /// information about the channel. +/// /// Every part of the widget uses a [StreamBuilder] to render the channel /// information as soon as it updates. /// /// By default the widget shows a backButton that calls [Navigator.pop]. -/// You can disable this button using the [showBackButton] property of just -/// override the behaviour -/// with [onBackPressed]. +/// You can disable this button using the [showBackButton] property. +/// Alternatively, you can override this behaviour via the [onBackPressed] +/// callback. /// -/// The widget components render the ui based on the first ancestor of type -/// [StreamChatTheme] and on its [StreamChatThemeData.channelHeaderTheme] -/// property. -/// Modify it to change the widget appearance. +/// The UI is rendered based on the first ancestor of type [StreamChatTheme] +/// and the [StreamChatThemeData.channelHeaderTheme] property. Modify it to +/// change the widget's appearance. /// {@endtemplate} class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget { - /// Creates a channel header + /// {@macro streamChannelHeader} const StreamChannelHeader({ super.key, this.showBackButton = true, @@ -76,23 +71,28 @@ class StreamChannelHeader extends StatelessWidget this.elevation = 1, }) : preferredSize = const Size.fromHeight(kToolbarHeight); - /// True if this header shows the leading back button + /// Whether to show the leading back button + /// + /// Defaults to `true` final bool showBackButton; - /// Callback to call when pressing the back button. + /// The action to perform when the back button is pressed. + /// /// By default it calls [Navigator.pop] final VoidCallback? onBackPressed; - /// Callback to call when the header is tapped. + /// The action to perform when the header is tapped. final VoidCallback? onTitleTap; - /// Callback to call when the image is tapped. + /// The action to perform when the image is tapped. final VoidCallback? onImageTap; - /// If true the typing indicator will be rendered if a user is typing + /// Whether to show the typing indicator + /// + /// Defaults to `true` final bool showTypingIndicator; - /// Show connection tile on header + /// Whether to show the connection state tile final bool showConnectionStateTile; /// Title widget @@ -107,8 +107,9 @@ class StreamChannelHeader extends StatelessWidget /// Leading widget final Widget? leading; - /// AppBar actions - /// By default it shows the [StreamChannelAvatar] + /// {@macro flutter.material.appbar.actions} + /// + /// The [StreamChannelAvatar] is shown by default final List? actions; /// The background color for this [StreamChannelHeader]. @@ -117,6 +118,9 @@ class StreamChannelHeader extends StatelessWidget /// The elevation for this [StreamChannelHeader]. final double elevation; + @override + final Size preferredSize; + @override Widget build(BuildContext context) { final effectiveCenterTitle = getEffectiveCenterTitle( @@ -131,7 +135,7 @@ class StreamChannelHeader extends StatelessWidget (showBackButton ? StreamBackButton( onPressed: onBackPressed, - showUnreads: true, + showUnreadCount: true, ) : const SizedBox()); @@ -215,7 +219,4 @@ class StreamChannelHeader extends StatelessWidget }, ); } - - @override - final Size preferredSize; } diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel/channel_info.dart new file mode 100644 index 00000000..36847649 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/channel/channel_info.dart @@ -0,0 +1,200 @@ +import 'package:collection/collection.dart' show IterableExtension; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template streamChannelInfo} +/// Displays information about the current [Channel]. +/// {@endtemplate} +class StreamChannelInfo extends StatelessWidget { + /// {@macro streamChannelInfo} + const StreamChannelInfo({ + super.key, + required this.channel, + this.textStyle, + this.showTypingIndicator = true, + this.parentId, + }); + + /// The channel to display information about + final Channel channel; + + /// The style of the text displayed + final TextStyle? textStyle; + + /// Whether to show the typing indicator + /// + /// Defaults to `true` + final bool showTypingIndicator; + + /// The ID of the parent message (in the case of a thread) + final String? parentId; + + @override + Widget build(BuildContext context) { + final client = StreamChat.of(context).client; + return BetterStreamBuilder>( + stream: channel.state!.membersStream, + initialData: channel.state!.members, + builder: (context, data) => StreamConnectionStatusBuilder( + statusBuilder: (context, status) { + switch (status) { + case ConnectionStatus.connected: + return _ConnectedTitleState( + channel: channel, + showTypingIndicator: showTypingIndicator, + textStyle: textStyle, + members: data, + parentId: parentId, + ); + case ConnectionStatus.connecting: + return _ConnectingTitleState(textStyle: textStyle); + case ConnectionStatus.disconnected: + return _DisconnectedTitleState( + client: client, + textStyle: textStyle, + ); + default: + return const Offstage(); + } + }, + ), + ); + } +} + +class _ConnectedTitleState extends StatelessWidget { + const _ConnectedTitleState({ + required this.channel, + required this.showTypingIndicator, + this.members, + this.textStyle, + this.parentId, + }); + + final Channel channel; + final List? members; + final TextStyle? textStyle; + final bool showTypingIndicator; + final String? parentId; + + @override + Widget build(BuildContext context) { + Widget? alternativeWidget; + + final memberCount = channel.memberCount; + if (memberCount != null && memberCount > 2) { + var text = context.translations.membersCountText(memberCount); + final onlineCount = + members?.where((m) => m.user?.online == true).length ?? 0; + if (onlineCount > 0) { + text += ', ${context.translations.watchersCountText(onlineCount)}'; + } + alternativeWidget = Text( + text, + style: StreamChannelHeaderTheme.of(context).subtitleStyle, + ); + } else { + final userId = StreamChat.of(context).currentUser?.id; + final otherMember = members?.firstWhereOrNull( + (element) => element.userId != userId, + ); + + if (otherMember != null) { + if (otherMember.user?.online == true) { + alternativeWidget = Text( + context.translations.userOnlineText, + style: textStyle, + ); + } else { + alternativeWidget = Text( + '${context.translations.userLastOnlineText} ' + '${Jiffy(otherMember.user?.lastActive).fromNow()}', + style: textStyle, + ); + } + } + } + + if (!showTypingIndicator) { + return alternativeWidget ?? const Offstage(); + } + + return StreamTypingIndicator( + parentId: parentId, + alternativeWidget: alternativeWidget, + style: textStyle, + ); + } +} + +class _ConnectingTitleState extends StatelessWidget { + const _ConnectingTitleState({ + this.textStyle, + }); + + final TextStyle? textStyle; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox( + height: 16, + width: 16, + child: Center( + child: CircularProgressIndicator(), + ), + ), + const SizedBox(width: 10), + Text( + context.translations.searchingForNetworkText, + style: textStyle, + ), + ], + ); + } +} + +class _DisconnectedTitleState extends StatelessWidget { + const _DisconnectedTitleState({ + required this.client, + this.textStyle, + }); + + final StreamChatClient client; + final TextStyle? textStyle; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + context.translations.offlineLabel, + style: textStyle, + ), + TextButton( + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: const VisualDensity( + horizontal: VisualDensity.minimumDensity, + vertical: VisualDensity.minimumDensity, + ), + ), + onPressed: () => client + ..closeConnection() + ..openConnection(), + child: Text( + context.translations.tryAgainLabel, + style: textStyle?.copyWith( + color: StreamChatTheme.of(context).colorTheme.accentPrimary, + ), + ), + ), + ], + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/channel_list_header.dart b/packages/stream_chat_flutter/lib/src/channel/channel_list_header.dart similarity index 74% rename from packages/stream_chat_flutter/lib/src/channel_list_header.dart rename to packages/stream_chat_flutter/lib/src/channel/channel_list_header.dart index 0701c08d..85f07ede 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_list_header.dart @@ -1,22 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/utils/utils.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// Widget builder for title -typedef TitleBuilder = Widget Function( - BuildContext context, - ConnectionStatus status, - StreamChatClient client, -); - -/// {@macro channel_list_header} -@Deprecated("Use 'StreamChannelListHeader' instead") -typedef ChannelListHeader = StreamChannelListHeader; - -/// {@template channel_list_header} -/// It shows the current [StreamChatClient] status. +/// {@template streamChannelListHeader} +/// Shows the current [StreamChatClient] status. /// /// ```dart /// class MyApp extends StatelessWidget { @@ -39,20 +28,19 @@ typedef ChannelListHeader = StreamChannelListHeader; /// ``` /// /// Usually you would use this widget as an [AppBar] inside a [Scaffold]. -/// However you can also use it as a normal widget. +/// However, you can also use it as a normal widget. /// -/// The widget by default uses the inherited [StreamChatClient] -/// to fetch information about the status. -/// However you can also pass your own [StreamChatClient] -/// if you don't have it in the widget tree. +/// Uses the inherited [StreamChatClient], by default, to fetch information +/// about the status of the [client]. You can also pass your own +/// [StreamChatClient] if you don't have it in the widget tree. /// -/// The widget components render the ui based on the first ancestor of type -/// [StreamChatTheme] and on its [StreamChannelListHeaderThemeData] property. -/// Modify it to change the widget appearance. +/// Renders the UI based on the first ancestor of type [StreamChatTheme] and +/// the [StreamChannelListHeaderThemeData] property. Modify it to change the +/// widget's appearance. /// {@endtemplate} class StreamChannelListHeader extends StatelessWidget implements PreferredSizeWidget { - /// Instantiates a ChannelListHeader + /// {@macro streamChannelListHeader} const StreamChannelListHeader({ super.key, this.client, @@ -69,23 +57,24 @@ class StreamChannelListHeader extends StatelessWidget this.elevation = 1, }); - /// Pass this if you don't have a [StreamChatClient] in your widget tree. + /// Use this if you don't have a [StreamChatClient] in your widget tree. final StreamChatClient? client; - /// Use this to build your own title as per different [ConnectionStatus] - final TitleBuilder? titleBuilder; + /// {@macro channelListHeaderTitleBuilder} + final ChannelListHeaderTitleBuilder? titleBuilder; - /// Callback to call when pressing the user avatar button. - /// By default it calls Scaffold.of(context).openDrawer() + /// The action to perform when pressing the user avatar button. + /// + /// By default it calls `Scaffold.of(context).openDrawer()`. final Function(User)? onUserAvatarTap; - /// Callback to call when pressing the new chat button. + /// The action to perform when pressing the "new chat" button. final VoidCallback? onNewChatButtonTap; - /// Show connection state tile + /// Whether to show the connection state tile final bool showConnectionStateTile; - /// Callback before navigation is performed + /// The function to execute before navigation is performed final VoidCallback? preNavigationCallback; /// Subtitle widget @@ -95,11 +84,13 @@ class StreamChannelListHeader extends StatelessWidget final bool? centerTitle; /// Leading widget - /// By default it shows the logged in user avatar + /// + /// By default it shows the logged in user's avatar final Widget? leading; - /// AppBar actions - /// By default it shows the new chat button + /// {@macro flutter.material.appbar.actions} + /// + /// The "new chat" button is shown by default. final List? actions; /// The background color for this [StreamChannelListHeader]. @@ -108,6 +99,9 @@ class StreamChannelListHeader extends StatelessWidget /// The elevation for this [StreamChannelListHeader]. final double elevation; + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); + @override Widget build(BuildContext context) { final _client = client ?? StreamChat.of(context).client; @@ -205,11 +199,11 @@ class StreamChannelListHeader extends StatelessWidget } switch (status) { case ConnectionStatus.connected: - return _buildConnectedTitleState(context); + return _ConnectedTitleState(); case ConnectionStatus.connecting: - return _buildConnectingTitleState(context); + return _ConnectingTitleState(); case ConnectionStatus.disconnected: - return _buildDisconnectedTitleState(context, _client); + return _DisconnectedTitleState(client: _client); default: return const Offstage(); } @@ -223,8 +217,11 @@ class StreamChannelListHeader extends StatelessWidget }, ); } +} - Widget _buildConnectedTitleState(BuildContext context) { +class _ConnectedTitleState extends StatelessWidget { + @override + Widget build(BuildContext context) { final chatThemeData = StreamChatTheme.of(context); return Text( context.translations.streamChatLabel, @@ -233,33 +230,43 @@ class StreamChannelListHeader extends StatelessWidget ), ); } +} - Widget _buildConnectingTitleState(BuildContext context) => Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox( - height: 16, - width: 16, - child: Center( - child: CircularProgressIndicator(), - ), +class _ConnectingTitleState extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox( + height: 16, + width: 16, + child: Center( + child: CircularProgressIndicator(), ), - const SizedBox(width: 10), - Text( - context.translations.searchingForNetworkText, - style: - StreamChannelListHeaderTheme.of(context).titleStyle?.copyWith( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ], - ); + ), + const SizedBox(width: 10), + Text( + context.translations.searchingForNetworkText, + style: StreamChannelListHeaderTheme.of(context).titleStyle?.copyWith( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ); + } +} - Widget _buildDisconnectedTitleState( - BuildContext context, - StreamChatClient client, - ) { +class _DisconnectedTitleState extends StatelessWidget { + const _DisconnectedTitleState({ + required this.client, + }); + + final StreamChatClient client; + + @override + Widget build(BuildContext context) { final chatThemeData = StreamChatTheme.of(context); final channelListHeaderTheme = StreamChannelListHeaderTheme.of(context); return Row( @@ -288,7 +295,4 @@ class StreamChannelListHeader extends StatelessWidget ], ); } - - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); } diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_name.dart b/packages/stream_chat_flutter/lib/src/channel/channel_name.dart new file mode 100644 index 00000000..7ff4e107 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/channel/channel_name.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template channelName} +/// Displays the current [Channel] name using a [Text] widget. +/// +/// The widget uses a [StreamBuilder] to render the channel information +/// image as soon as it updates. +/// {@endtemplate} +class ChannelName extends StatelessWidget { + /// {@macro channelName} + const ChannelName({ + super.key, + this.textStyle, + this.textOverflow = TextOverflow.ellipsis, + }); + + /// The style of the text displayed + final TextStyle? textStyle; + + /// How visual overflow should be handled. + final TextOverflow textOverflow; + + @override + Widget build(BuildContext context) { + final client = StreamChat.of(context); + final channel = StreamChannel.of(context).channel; + + assert(channel.state != null, 'Channel ${channel.id} is not initialized'); + + return BetterStreamBuilder( + stream: channel.nameStream, + initialData: channel.name, + builder: (context, channelName) => Text( + channelName, + style: textStyle, + overflow: textOverflow, + ), + noDataBuilder: (context) => _NameGenerator( + currentUser: client.currentUser!, + members: channel.state!.members, + textStyle: textStyle, + textOverflow: textOverflow, + ), + ); + } +} + +class _NameGenerator extends StatelessWidget { + const _NameGenerator({ + required this.currentUser, + required this.members, + this.textStyle, + this.textOverflow, + }); + + final User currentUser; + final List members; + final TextStyle? textStyle; + final TextOverflow? textOverflow; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + var channelName = context.translations.noTitleText; + final otherMembers = members.where( + (member) => member.userId != currentUser.id, + ); + + if (otherMembers.isNotEmpty) { + if (otherMembers.length == 1) { + final user = otherMembers.first.user; + if (user != null) { + channelName = user.name; + } + } else { + final maxWidth = constraints.maxWidth; + final maxChars = maxWidth / (textStyle?.fontSize ?? 1); + var currentChars = 0; + final currentMembers = []; + otherMembers.forEach((element) { + final newLength = currentChars + (element.user?.name.length ?? 0); + if (newLength < maxChars) { + currentChars = newLength; + currentMembers.add(element); + } + }); + + final exceedingMembers = + otherMembers.length - currentMembers.length; + channelName = + '${currentMembers.map((e) => e.user?.name).join(', ')} ' + '${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; + } + } + + return Text( + channelName, + style: textStyle, + overflow: textOverflow, + ); + }, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel/channel_preview.dart new file mode 100644 index 00000000..1fa06fdd --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/channel/channel_preview.dart @@ -0,0 +1,508 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:collection/collection.dart' + show IterableExtension, ListEquality; +import 'package:contextmenu/contextmenu.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/context_menu_items/stream_chat_context_menu_item.dart'; +import 'package:stream_chat_flutter/src/dialogs/dialogs.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template channelPreview} +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_preview.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_preview_paint.png) +/// +/// Shows a preview for the current [Channel]. +/// +/// Uses a [StreamBuilder] to render the channel information image as soon as +/// it updates. +/// +/// It is not recommended to use this widget directly as it is the +/// default channel preview widget used by [ChannelListView]. +/// +/// The UI is rendered based on the first ancestor of type [StreamChatTheme]. +/// Modify it to change the widget's appearance. +/// {@endtemplate} +class ChannelPreview extends StatelessWidget { + /// {@macro channelPreview} + const ChannelPreview({ + required this.channel, + super.key, + this.onTap, + this.onLongPress, + this.onViewInfoTap, + this.onImageTap, + this.title, + this.subtitle, + this.leading, + this.sendingIndicator, + this.trailing, + }); + + /// The action to perform when this widget is tapped or clicked. + final void Function(Channel)? onTap; + + /// The action to perform when this widget is long pressed. + final void Function(Channel)? onLongPress; + + /// The action to perform when 'View Info' is tapped or clicked. + final ViewInfoCallback? onViewInfoTap; + + /// The [Channel] being previewed. + final Channel channel; + + /// The action to perform when the image is tapped + final VoidCallback? onImageTap; + + /// Widget rendering the title + final Widget? title; + + /// Widget rendering the subtitle + final Widget? subtitle; + + /// Widget rendering the leading element. By default it shows the + /// [StreamChannelAvatar]. + final Widget? leading; + + /// Widget rendering the trailing element. By default it shows the date of + /// the last message. + final Widget? trailing; + + /// Widget rendering the sending indicator. By default it uses the + /// [StreamSendingIndicator] widget. + final Widget? sendingIndicator; + + @override + Widget build(BuildContext context) { + final channelPreviewTheme = StreamChannelPreviewTheme.of(context); + final streamChatState = StreamChat.of(context); + return BetterStreamBuilder( + stream: channel.isMutedStream, + initialData: channel.isMuted, + builder: (context, data) => AnimatedOpacity( + opacity: data ? 0.5 : 1, + duration: const Duration(milliseconds: 300), + child: ContextMenuArea( + verticalPadding: 0, + builder: (context) => [ + StreamChatContextMenuItem( + leading: StreamSvgIcon.user( + color: Colors.grey, + ), + title: Text(context.translations.viewInfoLabel), + onClick: () { + Navigator.of(context, rootNavigator: true).pop(); + if (onViewInfoTap != null) { + onViewInfoTap?.call(channel); + } else { + showDialog( + context: context, + builder: (_) => ChannelInfoDialog( + channel: channel, + ), + ); + } + }, + ), + StreamChatContextMenuItem( + leading: StreamSvgIcon.mute( + color: Colors.grey, + ), + title: channel.isGroup + ? Text( + context.translations + .toggleMuteUnmuteGroupText(isMuted: channel.isMuted), + ) + : Text( + context.translations + .toggleMuteUnmuteUserText(isMuted: channel.isMuted), + ), + onClick: () async { + Navigator.of(context, rootNavigator: true).pop(); + showDialog( + context: context, + builder: (_) => ConfirmationDialog( + titleText: channel.isGroup + ? context.translations + .toggleMuteUnmuteGroupText(isMuted: channel.isMuted) + : context.translations + .toggleMuteUnmuteUserText(isMuted: channel.isMuted), + promptText: channel.isGroup + ? context.translations.toggleMuteUnmuteGroupQuestion( + isMuted: channel.isMuted, + ) + : context.translations.toggleMuteUnmuteUserQuestion( + isMuted: channel.isMuted, + ), + affirmativeText: context.translations + .toggleMuteUnmuteAction(isMuted: channel.isMuted), + onConfirmation: () async { + try { + if (channel.isMuted) { + await channel.unmute(); + } else { + await channel.mute(); + } + } catch (e) { + showDialog( + context: context, + builder: (_) => MessageDialog( + messageText: e.toString(), + ), + ); + } + }, + ), + ); + }, + ), + if (channel.isGroup) + StreamChatContextMenuItem( + leading: StreamSvgIcon.userRemove( + color: Colors.red, + ), + title: Text( + context.translations.leaveGroupLabel, + style: const TextStyle( + color: Colors.red, + ), + ), + onClick: () { + Navigator.of(context, rootNavigator: true).pop(); + showDialog( + context: context, + builder: (_) => ConfirmationDialog( + titleText: context.translations.leaveGroupLabel, + promptText: + context.translations.leaveConversationQuestion, + affirmativeText: context.translations.leaveLabel, + onConfirmation: () async { + final userAsMember = channel.state?.members.firstWhere( + (e) => + e.user?.id == + StreamChat.of(context).currentUser?.id, + ); + try { + await channel.removeMembers([userAsMember!.user!.id]); + } catch (e) { + showDialog( + context: context, + builder: (_) => MessageDialog( + messageText: e.toString(), + ), + ); + } + }, + ), + ); + }, + ), + if (!channel.isGroup) + StreamChatContextMenuItem( + leading: StreamSvgIcon.delete( + color: Colors.red, + ), + title: Text( + context.translations.deleteConversationLabel, + style: const TextStyle( + color: Colors.red, + ), + ), + onClick: () { + Navigator.of(context, rootNavigator: true).pop(); + showDialog( + context: context, + builder: (_) => ConfirmationDialog( + titleText: context.translations.deleteConversationLabel, + promptText: + context.translations.deleteConversationQuestion, + affirmativeText: context.translations.deleteLabel, + onConfirmation: () async { + try { + await channel.delete(); + } catch (e) { + showDialog( + context: context, + builder: (_) => MessageDialog( + messageText: e.toString(), + ), + ); + } + }, + ), + ); + }, + ), + ], + child: ListTile( + visualDensity: VisualDensity.compact, + contentPadding: const EdgeInsets.symmetric( + horizontal: 8, + ), + onTap: () => onTap?.call(channel), + onLongPress: () => onLongPress?.call(channel), + leading: leading ?? + StreamChannelAvatar( + onTap: onImageTap, + channel: channel, + ), + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: title ?? + ChannelName( + textStyle: channelPreviewTheme.titleStyle, + ), + ), + BetterStreamBuilder>( + stream: channel.state?.membersStream, + initialData: channel.state?.members, + comparator: const ListEquality().equals, + builder: (context, members) { + if (members.isEmpty || + !members.any((Member e) => + e.user!.id == + channel.client.state.currentUser?.id)) { + return const SizedBox(); + } + return StreamUnreadIndicator( + cid: channel.cid, + ); + }, + ), + ], + ), + subtitle: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible(child: subtitle ?? _Subtitle(channel: channel)), + sendingIndicator ?? + Builder( + builder: (context) { + final lastMessage = + channel.state?.messages.lastWhereOrNull( + (m) => !m.isDeleted && !m.shadowed, + ); + if (lastMessage?.user?.id == + streamChatState.currentUser?.id) { + return Padding( + padding: const EdgeInsets.only(right: 4), + child: BetterStreamBuilder>( + stream: channel.state?.readStream, + initialData: channel.state?.read, + builder: (context, data) { + final readList = data.where((it) => + it.user.id != + channel.client.state.currentUser?.id && + (it.lastRead + .isAfter(lastMessage!.createdAt) || + it.lastRead.isAtSameMomentAs( + lastMessage.createdAt, + ))); + final isMessageRead = readList.length >= + (channel.memberCount ?? 0) - 1; + return StreamSendingIndicator( + message: lastMessage!, + size: channelPreviewTheme.indicatorIconSize, + isMessageRead: isMessageRead, + ); + }, + ), + ); + } + return const SizedBox(); + }, + ), + trailing ?? _Date(channel: channel), + ], + ), + ), + ), + ), + ); + } +} + +class _Date extends StatelessWidget { + const _Date({ + required this.channel, + }); + + final Channel channel; + + @override + Widget build(BuildContext context) { + return BetterStreamBuilder( + stream: channel.lastMessageAtStream, + initialData: channel.lastMessageAt, + builder: (context, data) { + final lastMessageAt = data.toLocal(); + + String stringDate; + final now = DateTime.now(); + + final startOfDay = DateTime(now.year, now.month, now.day); + + if (lastMessageAt.millisecondsSinceEpoch >= + startOfDay.millisecondsSinceEpoch) { + stringDate = Jiffy(lastMessageAt.toLocal()).jm; + } else if (lastMessageAt.millisecondsSinceEpoch >= + startOfDay + .subtract(const Duration(days: 1)) + .millisecondsSinceEpoch) { + stringDate = context.translations.yesterdayLabel; + } else if (startOfDay.difference(lastMessageAt).inDays < 7) { + stringDate = Jiffy(lastMessageAt.toLocal()).EEEE; + } else { + stringDate = Jiffy(lastMessageAt.toLocal()).yMd; + } + + return Text( + stringDate, + style: StreamChannelPreviewTheme.of(context).lastMessageAtStyle, + ); + }, + ); + } +} + +class _Subtitle extends StatelessWidget { + const _Subtitle({ + required this.channel, + }); + + final Channel channel; + + @override + Widget build(BuildContext context) { + final channelPreviewTheme = StreamChannelPreviewTheme.of(context); + if (channel.isMuted) { + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + StreamSvgIcon.mute( + size: 16, + ), + Text( + ' ${context.translations.channelIsMutedText}', + style: channelPreviewTheme.subtitleStyle, + ), + ], + ); + } + return StreamTypingIndicator( + channel: channel, + alternativeWidget: _LastMessage( + channel: channel, + ), + style: channelPreviewTheme.subtitleStyle, + ); + } +} + +class _LastMessage extends StatelessWidget { + const _LastMessage({ + required this.channel, + }); + + final Channel channel; + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.centerLeft, + child: BetterStreamBuilder>( + stream: channel.state!.messagesStream, + initialData: channel.state!.messages, + builder: (context, data) { + final lastMessage = + data.lastWhereOrNull((m) => !m.shadowed && !m.isDeleted); + if (lastMessage == null) { + return const SizedBox(); + } + + var text = lastMessage.text; + final parts = [ + ...lastMessage.attachments.map((e) { + if (e.type == 'image') { + return '📷'; + } else if (e.type == 'video') { + return '🎬'; + } else if (e.type == 'giphy') { + return '[GIF]'; + } + return e == lastMessage.attachments.last + ? (e.title ?? 'File') + : '${e.title ?? 'File'} , '; + }), + lastMessage.text ?? '', + ]; + + text = parts.join(' '); + + final channelPreviewTheme = StreamChannelPreviewTheme.of(context); + return Text.rich( + _getDisplayText( + text, + lastMessage.mentionedUsers, + lastMessage.attachments, + channelPreviewTheme.subtitleStyle?.copyWith( + color: channelPreviewTheme.subtitleStyle?.color, + fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) + ? FontStyle.italic + : FontStyle.normal, + ), + channelPreviewTheme.subtitleStyle?.copyWith( + color: channelPreviewTheme.subtitleStyle?.color, + fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) + ? FontStyle.italic + : FontStyle.normal, + fontWeight: FontWeight.bold, + ), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + ); + }, + ), + ); + } + + TextSpan _getDisplayText( + String text, + List mentions, + List attachments, + TextStyle? normalTextStyle, + TextStyle? mentionsTextStyle, + ) { + final textList = text.split(' '); + final resList = []; + for (final e in textList) { + if (mentions.isNotEmpty && + mentions.any((element) => '@${element.name}' == e)) { + resList.add(TextSpan( + text: '$e ', + style: mentionsTextStyle, + )); + } else if (attachments.isNotEmpty && + attachments + .where((e) => e.title != null) + .any((element) => element.title == e)) { + resList.add(TextSpan( + text: '$e ', + style: normalTextStyle?.copyWith(fontStyle: FontStyle.italic), + )); + } else { + resList.add(TextSpan( + text: e == textList.last ? e : '$e ', + style: normalTextStyle, + )); + } + } + + return TextSpan(children: resList); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/stream_channel_avatar.dart b/packages/stream_chat_flutter/lib/src/channel/stream_channel_avatar.dart similarity index 99% rename from packages/stream_chat_flutter/lib/src/v4/stream_channel_avatar.dart rename to packages/stream_chat_flutter/lib/src/channel/stream_channel_avatar.dart index 452e19a3..5ad7a000 100644 --- a/packages/stream_chat_flutter/lib/src/v4/stream_channel_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/channel/stream_channel_avatar.dart @@ -1,6 +1,5 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/group_avatar.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_image.png) diff --git a/packages/stream_chat_flutter/lib/src/v4/stream_channel_name.dart b/packages/stream_chat_flutter/lib/src/channel/stream_channel_name.dart similarity index 97% rename from packages/stream_chat_flutter/lib/src/v4/stream_channel_name.dart rename to packages/stream_chat_flutter/lib/src/channel/stream_channel_name.dart index b7536df3..fd8a299f 100644 --- a/packages/stream_chat_flutter/lib/src/v4/stream_channel_name.dart +++ b/packages/stream_chat_flutter/lib/src/channel/stream_channel_name.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; diff --git a/packages/stream_chat_flutter/lib/src/v4/stream_message_preview_text.dart b/packages/stream_chat_flutter/lib/src/channel/stream_message_preview_text.dart similarity index 98% rename from packages/stream_chat_flutter/lib/src/v4/stream_message_preview_text.dart rename to packages/stream_chat_flutter/lib/src/channel/stream_message_preview_text.dart index 4e036c5c..cecc619d 100644 --- a/packages/stream_chat_flutter/lib/src/v4/stream_message_preview_text.dart +++ b/packages/stream_chat_flutter/lib/src/channel/stream_message_preview_text.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// A widget that renders a preview of the message text. diff --git a/packages/stream_chat_flutter/lib/src/channel_avatar.dart b/packages/stream_chat_flutter/lib/src/channel_avatar.dart deleted file mode 100644 index fcd20e68..00000000 --- a/packages/stream_chat_flutter/lib/src/channel_avatar.dart +++ /dev/null @@ -1,203 +0,0 @@ -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/group_avatar.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_image.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_image_paint.png) -/// -/// It shows the current [Channel] image. -/// -/// ```dart -/// class MyApp extends StatelessWidget { -/// final StreamChatClient client; -/// final Channel channel; -/// -/// MyApp(this.client, this.channel); -/// -/// @override -/// Widget build(BuildContext context) { -/// return MaterialApp( -/// debugShowCheckedModeBanner: false, -/// home: StreamChat( -/// client: client, -/// child: StreamChannel( -/// channel: channel, -/// child: Center( -/// child: ChannelAvatar( -/// channel: channel, -/// ), -/// ), -/// ), -/// ), -/// ); -/// } -/// } -/// ``` -/// -/// The widget uses a [StreamBuilder] to render the channel information -/// image as soon as it updates. -/// -/// By default the widget radius size is 40x40 pixels. -/// Set the property [constraints] to set a custom dimension. -/// -/// The widget renders the ui based on the first ancestor of type -/// [StreamChatTheme]. -/// Modify it to change the widget appearance. - -@Deprecated( - "'ChannelAvatar' is deprecated and shouldn't be used. " - "Please use 'StreamChannelAvatar' instead.", -) -class ChannelAvatar extends StatelessWidget { - /// Instantiate a new ChannelImage - const ChannelAvatar({ - super.key, - this.channel, - this.constraints, - this.onTap, - this.borderRadius, - this.selected = false, - this.selectionColor, - this.selectionThickness = 4, - }); - - /// [BorderRadius] to display the widget - final BorderRadius? borderRadius; - - /// The channel to show the image of - final Channel? channel; - - /// The diameter of the image - final BoxConstraints? constraints; - - /// The function called when the image is tapped - final VoidCallback? onTap; - - /// If image is selected - final bool selected; - - /// Selection color for image - final Color? selectionColor; - - /// Thickness of selection image - final double selectionThickness; - - @override - Widget build(BuildContext context) { - final streamChat = StreamChat.of(context); - final channel = this.channel ?? StreamChannel.of(context).channel; - - assert(channel.state != null, 'Channel ${channel.id} is not initialized'); - - final chatThemeData = StreamChatTheme.of(context); - final colorTheme = chatThemeData.colorTheme; - final previewTheme = chatThemeData.channelPreviewTheme.avatarTheme; - - return BetterStreamBuilder( - stream: channel.imageStream, - initialData: channel.image, - builder: (context, channelImage) { - Widget child = ClipRRect( - borderRadius: borderRadius ?? previewTheme?.borderRadius, - child: Container( - constraints: constraints ?? previewTheme?.constraints, - decoration: BoxDecoration(color: colorTheme.accentPrimary), - child: InkWell( - onTap: onTap, - child: CachedNetworkImage( - imageUrl: channelImage, - errorWidget: (_, __, ___) => Center( - child: Text( - channel.name?[0] ?? '', - style: TextStyle( - color: colorTheme.barsBg, - fontWeight: FontWeight.bold, - ), - ), - ), - fit: BoxFit.cover, - ), - ), - ), - ); - - if (selected) { - child = ClipRRect( - key: const Key('selectedImage'), - borderRadius: BorderRadius.circular(selectionThickness) + - (borderRadius ?? - previewTheme?.borderRadius ?? - BorderRadius.zero), - child: Container( - constraints: constraints ?? previewTheme?.constraints, - color: selectionColor ?? colorTheme.accentPrimary, - child: Padding( - padding: EdgeInsets.all(selectionThickness), - child: child, - ), - ), - ); - } - return child; - }, - noDataBuilder: (context) { - final currentUser = streamChat.currentUser!; - final otherMembers = channel.state!.members - .where((it) => it.userId != currentUser.id) - .toList(growable: false); - - // our own space, no other members - if (otherMembers.isEmpty) { - return BetterStreamBuilder( - stream: streamChat.client.state.currentUserStream.map((it) => it!), - initialData: currentUser, - builder: (context, user) => StreamUserAvatar( - borderRadius: borderRadius ?? previewTheme?.borderRadius, - user: user, - constraints: constraints ?? previewTheme?.constraints, - onTap: onTap != null ? (_) => onTap!() : null, - selected: selected, - selectionColor: selectionColor ?? colorTheme.accentPrimary, - selectionThickness: selectionThickness, - ), - ); - } - - // 1-1 Conversation - if (otherMembers.length == 1) { - final member = otherMembers.first; - return BetterStreamBuilder( - stream: channel.state!.membersStream.map( - (members) => members.firstWhere( - (it) => it.userId == member.userId, - orElse: () => member, - ), - ), - initialData: member, - builder: (context, member) => StreamUserAvatar( - borderRadius: borderRadius ?? previewTheme?.borderRadius, - user: member.user!, - constraints: constraints ?? previewTheme?.constraints, - onTap: onTap != null ? (_) => onTap!() : null, - selected: selected, - selectionColor: selectionColor ?? colorTheme.accentPrimary, - selectionThickness: selectionThickness, - ), - ); - } - - // Group conversation - return StreamGroupAvatar( - members: otherMembers, - borderRadius: borderRadius ?? previewTheme?.borderRadius, - constraints: constraints ?? previewTheme?.constraints, - onTap: onTap, - selected: selected, - selectionColor: selectionColor ?? colorTheme.accentPrimary, - selectionThickness: selectionThickness, - ); - }, - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart deleted file mode 100644 index a4275f88..00000000 --- a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart +++ /dev/null @@ -1,266 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/channel_info.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// Bottom Sheet with options -@Deprecated("Use 'StreamChannelInfoBottomSheet' instead") -class ChannelBottomSheet extends StatefulWidget { - /// Constructor for creating bottom sheet - const ChannelBottomSheet({super.key, this.onViewInfoTap}); - - /// Callback when 'View Info' is tapped - final VoidCallback? onViewInfoTap; - - @override - _ChannelBottomSheetState createState() => _ChannelBottomSheetState(); -} - -// ignore: deprecated_member_use_from_same_package -class _ChannelBottomSheetState extends State { - bool _showActions = true; - - late StreamChannelState _streamChannelState; - late StreamChannelPreviewThemeData _channelPreviewThemeData; - late StreamChatThemeData _streamChatThemeData; - late StreamChatState _streamChatState; - - @override - Widget build(BuildContext context) { - final channel = _streamChannelState.channel; - - final members = channel.state?.members ?? []; - - final userAsMember = members - .firstWhere((e) => e.user?.id == _streamChatState.currentUser?.id); - - return Material( - color: _streamChatThemeData.colorTheme.barsBg, - clipBehavior: Clip.antiAlias, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - ), - child: !_showActions - ? const SizedBox() - : ListView( - shrinkWrap: true, - children: [ - const SizedBox( - height: 24, - ), - Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: StreamChannelName( - channel: channel, - textStyle: _streamChatThemeData.textTheme.headlineBold, - ), - ), - ), - const SizedBox( - height: 5, - ), - Center( - child: StreamChannelInfo( - showTypingIndicator: false, - channel: _streamChannelState.channel, - textStyle: _channelPreviewThemeData.subtitleStyle, - ), - ), - const SizedBox( - height: 17, - ), - if (channel.isDistinct && channel.memberCount == 2) - Column( - children: [ - StreamUserAvatar( - user: members - .firstWhere( - (e) => e.user?.id != userAsMember.user?.id, - ) - .user!, - constraints: const BoxConstraints( - maxHeight: 64, - maxWidth: 64, - ), - borderRadius: BorderRadius.circular(32), - onlineIndicatorConstraints: - BoxConstraints.tight(const Size(12, 12)), - ), - const SizedBox( - height: 6, - ), - Text( - members - .firstWhere( - (e) => e.user?.id != userAsMember.user?.id, - ) - .user - ?.name ?? - '', - style: _streamChatThemeData.textTheme.footnoteBold, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - if (!(channel.isDistinct && channel.memberCount == 2)) - Container( - height: 94, - alignment: Alignment.center, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: members.length, - shrinkWrap: true, - itemBuilder: (context, index) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Column( - children: [ - StreamUserAvatar( - user: members[index].user!, - constraints: const BoxConstraints.tightFor( - height: 64, - width: 64, - ), - borderRadius: BorderRadius.circular(32), - onlineIndicatorConstraints: - BoxConstraints.tight(const Size(12, 12)), - ), - const SizedBox( - height: 6, - ), - Text( - members[index].user?.name ?? '', - style: - _streamChatThemeData.textTheme.footnoteBold, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ), - ), - const SizedBox( - height: 24, - ), - StreamOptionListTile( - leading: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: StreamSvgIcon.user( - color: _streamChatThemeData.colorTheme.textLowEmphasis, - ), - ), - title: context.translations.viewInfoLabel, - onTap: widget.onViewInfoTap, - ), - if (!channel.isDistinct && - channel.ownCapabilities - .contains(PermissionType.leaveChannel)) - StreamOptionListTile( - leading: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: StreamSvgIcon.userRemove( - color: _streamChatThemeData.colorTheme.textLowEmphasis, - ), - ), - title: context.translations.leaveGroupLabel, - onTap: () async { - setState(() { - _showActions = false; - }); - await _showLeaveDialog(); - setState(() { - _showActions = true; - }); - }, - ), - if (channel.ownCapabilities - .contains(PermissionType.deleteChannel)) - StreamOptionListTile( - leading: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: StreamSvgIcon.delete( - color: _streamChatThemeData.colorTheme.accentError, - ), - ), - title: context.translations.deleteConversationLabel, - titleColor: _streamChatThemeData.colorTheme.accentError, - onTap: () async { - setState(() { - _showActions = false; - }); - await _showDeleteDialog(); - setState(() { - _showActions = true; - }); - }, - ), - StreamOptionListTile( - leading: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: StreamSvgIcon.closeSmall( - color: _streamChatThemeData.colorTheme.textLowEmphasis, - ), - ), - title: context.translations.cancelLabel, - onTap: () { - Navigator.pop(context); - }, - ), - ], - ), - ); - } - - @override - void didChangeDependencies() { - _streamChannelState = StreamChannel.of(context); - _streamChatThemeData = StreamChatTheme.of(context); - _channelPreviewThemeData = StreamChannelPreviewTheme.of(context); - _streamChatState = StreamChat.of(context); - super.didChangeDependencies(); - } - - Future _showDeleteDialog() async { - final res = await showConfirmationDialog( - context, - title: context.translations.deleteConversationLabel, - okText: context.translations.deleteLabel, - question: context.translations.deleteConversationQuestion, - cancelText: context.translations.cancelLabel, - icon: StreamSvgIcon.delete( - color: _streamChatThemeData.colorTheme.accentError, - ), - ); - final channel = _streamChannelState.channel; - if (res == true) { - await channel.delete(); - Navigator.pop(context); - } - } - - Future _showLeaveDialog() async { - final res = await showConfirmationDialog( - context, - title: context.translations.leaveConversationLabel, - okText: context.translations.leaveLabel, - question: context.translations.leaveConversationQuestion, - cancelText: context.translations.cancelLabel, - icon: StreamSvgIcon.userRemove( - color: _streamChatThemeData.colorTheme.accentError, - ), - ); - if (res == true) { - final channel = _streamChannelState.channel; - final user = _streamChatState.currentUser; - if (user != null) { - await channel.removeMembers([user.id]); - } - Navigator.pop(context); - } - } -} diff --git a/packages/stream_chat_flutter/lib/src/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel_info.dart deleted file mode 100644 index 2d45d327..00000000 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ /dev/null @@ -1,160 +0,0 @@ -import 'package:collection/collection.dart' show IterableExtension; -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@macro channel_info} -@Deprecated("Use 'StreamChannelInfo' instead") -typedef ChannelInfo = StreamChannelInfo; - -/// {@template channel_info} -/// Widget which shows channel info -/// {@endtemplate} -class StreamChannelInfo extends StatelessWidget { - /// Constructor which creates a [StreamChannelInfo] widget - const StreamChannelInfo({ - super.key, - required this.channel, - this.textStyle, - this.showTypingIndicator = true, - this.parentId, - }); - - /// The channel about which the info is to be displayed - final Channel channel; - - /// The style of the text displayed - final TextStyle? textStyle; - - /// If true the typing indicator will be rendered if a user is typing - final bool showTypingIndicator; - - /// Id of the parent message in case of a thread - final String? parentId; - - @override - Widget build(BuildContext context) { - final client = StreamChat.of(context).client; - return BetterStreamBuilder>( - stream: channel.state!.membersStream, - initialData: channel.state!.members, - builder: (context, data) => StreamConnectionStatusBuilder( - statusBuilder: (context, status) { - switch (status) { - case ConnectionStatus.connected: - return _buildConnectedTitleState(context, data); - case ConnectionStatus.connecting: - return _buildConnectingTitleState(context); - case ConnectionStatus.disconnected: - return _buildDisconnectedTitleState(context, client); - default: - return const Offstage(); - } - }, - ), - ); - } - - Widget _buildConnectedTitleState( - BuildContext context, - List? members, - ) { - Widget? alternativeWidget; - - final memberCount = channel.memberCount; - if (memberCount != null && memberCount > 2) { - var text = context.translations.membersCountText(memberCount); - final onlineCount = - members?.where((m) => m.user?.online == true).length ?? 0; - if (channel.ownCapabilities.contains(PermissionType.connectEvents) && - onlineCount > 0) { - text += ', ${context.translations.watchersCountText(onlineCount)}'; - } - alternativeWidget = Text( - text, - style: StreamChannelHeaderTheme.of(context).subtitleStyle, - ); - } else { - final userId = StreamChat.of(context).currentUser?.id; - final otherMember = members?.firstWhereOrNull( - (element) => element.userId != userId, - ); - - if (otherMember != null) { - if (otherMember.user?.online == true) { - alternativeWidget = Text( - context.translations.userOnlineText, - style: textStyle, - ); - } else { - alternativeWidget = Text( - '${context.translations.userLastOnlineText} ' - '${Jiffy(otherMember.user?.lastActive).fromNow()}', - style: textStyle, - ); - } - } - } - - if (!showTypingIndicator) { - return alternativeWidget ?? const Offstage(); - } - - return StreamTypingIndicator( - parentId: parentId, - style: textStyle, - alternativeWidget: alternativeWidget, - ); - } - - Widget _buildConnectingTitleState(BuildContext context) => Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox( - height: 16, - width: 16, - child: Center( - child: CircularProgressIndicator(), - ), - ), - const SizedBox(width: 10), - Text( - context.translations.searchingForNetworkText, - style: textStyle, - ), - ], - ); - - Widget _buildDisconnectedTitleState( - BuildContext context, - StreamChatClient client, - ) => - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - context.translations.offlineLabel, - style: textStyle, - ), - TextButton( - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - visualDensity: const VisualDensity( - horizontal: VisualDensity.minimumDensity, - vertical: VisualDensity.minimumDensity, - ), - ), - onPressed: () => client - ..closeConnection() - ..openConnection(), - child: Text( - context.translations.tryAgainLabel, - style: textStyle?.copyWith( - color: StreamChatTheme.of(context).colorTheme.accentPrimary, - ), - ), - ), - ], - ); -} diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart deleted file mode 100644 index 3f504331..00000000 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ /dev/null @@ -1,742 +0,0 @@ -// ignore: lines_longer_than_80_chars -// ignore_for_file: deprecated_member_use_from_same_package, deprecated_member_use - -import 'package:flutter/material.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; -import 'package:shimmer/shimmer.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// Callback called when tapping on a channel -typedef ChannelTapCallback = void Function(Channel, Widget?); - -/// Callback called when tapping on a channel -typedef ChannelInfoCallback = void Function(Channel); - -/// Builder used to create a custom [StreamChannelPreview] from a [Channel] -typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel); - -/// Callback for when 'View Info' is tapped -typedef ViewInfoCallback = void Function(Channel); - -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_list_view.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_list_view_paint.png) -/// -/// It shows the list of current channels. -/// -/// ```dart -/// class ChannelListPage extends StatelessWidget { -/// @override -/// Widget build(BuildContext context) { -/// return Scaffold( -/// body: ChannelListView( -/// filter: { -/// 'members': { -/// '\$in': [StreamChat.of(context).user.id], -/// } -/// }, -/// sort: [SortOption('last_message_at')], -/// pagination: PaginationParams( -/// limit: 20, -/// ), -/// channelWidget: ChannelPage(), -/// ), -/// ); -/// } -/// } -/// ``` -/// -/// -/// Make sure to have a [StreamChat] ancestor in order to provide the -/// information about the channels. -/// The widget uses a [ListView.custom] to render the list of channels. -/// -/// The widget components render the ui based on the first ancestor of -/// type [StreamChatTheme]. -/// Modify it to change the widget appearance. -@Deprecated("Use 'StreamChannelListView' instead") -class ChannelListView extends StatefulWidget { - /// Instantiate a new ChannelListView - @Deprecated("Use 'StreamChannelListView' instead") - ChannelListView({ - super.key, - this.filter, - this.sort, - this.state = true, - this.watch = true, - this.presence = false, - this.memberLimit, - this.messageLimit, - @Deprecated( - "'pagination' is deprecated and shouldn't be used. " - "This property is no longer used, Please use 'limit' instead", - ) - this.pagination, - int? limit, - this.onChannelTap, - this.onChannelLongPress, - this.channelWidget, - this.channelPreviewBuilder, - this.separatorBuilder, - this.onImageTap, - this.onStartChatPressed, - this.swipeToAction = false, - this.pullToRefresh = true, - this.crossAxisCount = 1, - this.padding, - this.selectedChannels = const [], - this.onViewInfoTap, - this.errorBuilder, - this.emptyBuilder, - this.loadingBuilder, - this.listBuilder, - this.onMoreDetailsPressed, - this.onDeletePressed, - this.swipeActions, - this.channelListController, - }) : limit = limit ?? pagination?.limit ?? 25; - - /// If true a default swipe to action behaviour will be added to this widget - final bool swipeToAction; - - /// The query filters to use. - /// You can query on any of the custom fields you've defined on the [Channel]. - /// You can also filter other built-in channel fields. - final Filter? filter; - - /// The sorting used for the channels matching the filters. - /// Sorting is based on field and direction, multiple sorting options - /// can be provided. - /// You can sort based on last_updated, last_message_at, updated_at, - /// created_at or member_count. - /// Direction can be ascending or descending. - final List>? sort; - - /// If true returns the Channel state - final bool state; - - /// If true listen to changes to this Channel in real time. - final bool watch; - - /// If true you’ll receive user presence updates via the websocket events - final bool presence; - - /// Number of members to fetch in each channel - final int? memberLimit; - - /// Number of messages to fetch in each channel - final int? messageLimit; - - /// Pagination parameters - /// limit: the number of channels to return (max is 30) - /// offset: the offset (max is 1000) - /// message_limit: how many messages should be included to each channel - @Deprecated( - "'pagination' is deprecated and shouldn't be used. " - "This property is no longer used, Please use 'limit' instead", - ) - final PaginationParams? pagination; - - /// The amount of channels requested per API call. - final int limit; - - /// Function called when tapping on a channel - /// By default it calls [Navigator.push] building a [MaterialPageRoute] - /// with the widget [channelWidget] as child. - final ChannelTapCallback? onChannelTap; - - /// Function called when long pressing on a channel - final Function(Channel)? onChannelLongPress; - - /// Widget used when opening a channel - final Widget? channelWidget; - - /// Builder used to create a custom channel preview - final ChannelPreviewBuilder? channelPreviewBuilder; - - /// Builder used to create a custom item separator - final Function(BuildContext, int)? separatorBuilder; - - /// The function called when the image is tapped - final Function(Channel)? onImageTap; - - /// Set it to false to disable the pull-to-refresh widget - final bool pullToRefresh; - - /// Callback used in the default empty list widget - final VoidCallback? onStartChatPressed; - - /// The number of children in the cross axis. - final int crossAxisCount; - - /// The amount of space by which to inset the children. - final EdgeInsetsGeometry? padding; - - /// List of selected channels which are displayed differently - final List selectedChannels; - - /// Callback for when 'View Info' is tapped - final ViewInfoCallback? onViewInfoTap; - - /// The builder that will be used in case of error - final ErrorBuilder? errorBuilder; - - /// The builder that will be used in case of loading - final WidgetBuilder? loadingBuilder; - - /// The builder which is used when list of channels loads - final Function(BuildContext, List)? listBuilder; - - /// The builder used when the channel list is empty. - final WidgetBuilder? emptyBuilder; - - /// Callback used when the more details slidable option is pressed - final ChannelInfoCallback? onMoreDetailsPressed; - - /// Callback used when the delete slidable option is pressed - final ChannelInfoCallback? onDeletePressed; - - /// List of actions for slidable - final List? swipeActions; - - /// A [ChannelListController] allows reloading and pagination. - /// Use [ChannelListController.loadData] and - /// [ChannelListController.paginateData] respectively for reloading and - /// pagination. - final ChannelListController? channelListController; - - @override - _ChannelListViewState createState() => _ChannelListViewState(); -} - -class _ChannelListViewState extends State { - late final _defaultController = ChannelListController(); - - ChannelListController get _channelListController => - widget.channelListController ?? _defaultController; - - @override - Widget build(BuildContext context) { - Widget child = ChannelListCore( - filter: widget.filter, - sort: widget.sort, - state: widget.state, - watch: widget.watch, - presence: widget.presence, - memberLimit: widget.memberLimit, - messageLimit: widget.messageLimit, - limit: widget.limit, - channelListController: _channelListController, - listBuilder: widget.listBuilder ?? _buildListView, - emptyBuilder: widget.emptyBuilder ?? _buildEmptyWidget, - errorBuilder: widget.errorBuilder ?? _buildErrorWidget, - loadingBuilder: widget.loadingBuilder ?? _buildLoadingWidget, - ); - - if (widget.pullToRefresh) { - child = RefreshIndicator( - onRefresh: () => _channelListController.loadData!(), - child: child, - ); - } - - child = LazyLoadScrollView( - onEndOfPage: () => _channelListController.paginateData!(), - child: child, - ); - - final backgroundColor = - StreamChannelListViewTheme.of(context).backgroundColor; - - if (backgroundColor != null) { - return ColoredBox( - color: backgroundColor, - child: child, - ); - } - - return child; - } - - Widget _buildListView(BuildContext context, List channels) { - if (widget.crossAxisCount > 1) { - return GridView.builder( - padding: widget.padding, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: widget.crossAxisCount, - ), - itemCount: channels.length, - physics: const AlwaysScrollableScrollPhysics(), - itemBuilder: (context, index) => - _gridItemBuilder(context, index, channels), - ); - } - return SlidableAutoCloseBehavior( - child: ListView.separated( - padding: widget.padding, - physics: const AlwaysScrollableScrollPhysics(), - // all channels + progress loader - itemCount: channels.length + 1, - separatorBuilder: (_, index) { - if (widget.separatorBuilder != null) { - return widget.separatorBuilder!(context, index); - } - return _separatorBuilder(context, index); - }, - itemBuilder: (context, index) => - _listItemBuilder(context, index, channels), - ), - ); - } - - Widget _buildEmptyWidget(BuildContext context) => LayoutBuilder( - builder: (context, viewportConstraints) { - final chatThemeData = StreamChatTheme.of(context); - return SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - child: Stack( - children: [ - ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.all(8), - child: StreamSvgIcon.message( - size: 136, - color: chatThemeData.colorTheme.disabled, - ), - ), - Padding( - padding: const EdgeInsets.all(8), - child: Text( - context.translations.letsStartChattingLabel, - style: chatThemeData.textTheme.headline, - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 52, - ), - child: Text( - context.translations.sendingFirstMessageLabel, - textAlign: TextAlign.center, - style: chatThemeData.textTheme.body.copyWith( - color: chatThemeData.colorTheme.textLowEmphasis, - ), - ), - ), - ], - ), - ), - if (widget.onStartChatPressed != null) - Positioned( - right: 0, - left: 0, - bottom: 32, - child: Center( - child: TextButton( - onPressed: widget.onStartChatPressed, - child: Text( - context.translations.startAChatLabel, - style: chatThemeData.textTheme.bodyBold.copyWith( - color: chatThemeData.colorTheme.accentPrimary, - ), - ), - ), - ), - ), - ], - ), - ); - }, - ); - - Widget _buildLoadingWidget(BuildContext context) => ListView( - padding: widget.padding, - physics: const AlwaysScrollableScrollPhysics(), - children: List.generate( - 25, - (i) { - if (widget.crossAxisCount == 1) { - if (i % 2 != 0) { - if (widget.separatorBuilder != null) { - return widget.separatorBuilder!(context, i); - } - return _separatorBuilder(context, i); - } - } - return _buildLoadingItem(context); - }, - ), - ); - - Shimmer _buildLoadingItem(BuildContext context) { - final chatThemeData = StreamChatTheme.of(context); - if (widget.crossAxisCount > 1) { - return Shimmer.fromColors( - baseColor: chatThemeData.colorTheme.disabled, - highlightColor: chatThemeData.colorTheme.inputBg, - child: Column( - children: [ - const SizedBox(height: 4), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - for (int i = 0; i < widget.crossAxisCount; i++) - Container( - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - constraints: const BoxConstraints.tightFor( - height: 70, - width: 70, - ), - ), - ], - ), - const SizedBox( - height: 16, - ), - ], - ), - ); - } else { - return Shimmer.fromColors( - baseColor: chatThemeData.colorTheme.disabled, - highlightColor: chatThemeData.colorTheme.inputBg, - child: ListTile( - leading: Container( - decoration: BoxDecoration( - color: chatThemeData.colorTheme.barsBg, - shape: BoxShape.circle, - ), - constraints: const BoxConstraints.tightFor( - height: 40, - width: 40, - ), - ), - contentPadding: const EdgeInsets.only( - left: 8, - right: 8, - ), - title: Align( - alignment: Alignment.centerLeft, - child: Container( - decoration: BoxDecoration( - color: chatThemeData.colorTheme.barsBg, - borderRadius: BorderRadius.circular(11), - ), - constraints: const BoxConstraints.tightFor( - height: 16, - width: 82, - ), - ), - ), - subtitle: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Align( - alignment: Alignment.centerLeft, - child: Container( - decoration: BoxDecoration( - color: chatThemeData.colorTheme.barsBg, - borderRadius: BorderRadius.circular(11), - ), - constraints: const BoxConstraints.expand( - height: 16, - ), - ), - ), - ), - Container( - margin: const EdgeInsets.only(left: 16), - decoration: BoxDecoration( - color: chatThemeData.colorTheme.barsBg, - borderRadius: BorderRadius.circular(11), - ), - constraints: const BoxConstraints.tightFor( - height: 16, - width: 42, - ), - ), - ], - ), - ), - ); - } - } - - Widget _buildErrorWidget(BuildContext context, Object error) => Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text.rich( - TextSpan( - children: [ - const WidgetSpan( - child: Padding( - padding: EdgeInsets.only( - right: 2, - ), - child: Icon(Icons.error_outline), - ), - ), - TextSpan(text: context.translations.loadingChannelsError), - ], - ), - style: Theme.of(context).textTheme.headline6, - ), - TextButton( - onPressed: () => _channelListController.loadData!(), - child: Text(context.translations.retryLabel), - ), - ], - ), - ); - - Widget _listItemBuilder(BuildContext context, int i, List channels) { - final channelsBloc = ChannelsBloc.of(context); - - if (i == channels.length) { - return _buildQueryProgressIndicator(context, channelsBloc); - } - - final onTap = _getChannelTap(context); - final chatThemeData = StreamChatTheme.of(context); - final backgroundColor = chatThemeData.colorTheme.inputBg; - final channel = channels[i]; - - final canDeleteChannel = - channel.ownCapabilities.contains(PermissionType.deleteChannel); - - final actionPaneChildren = - widget.swipeActions?.length ?? (canDeleteChannel ? 2 : 1); - final actionPaneExtentRatio = actionPaneChildren > 5 - ? 1 / actionPaneChildren - : actionPaneChildren * 0.2; - - return StreamChannel( - key: ValueKey('CHANNEL-${channel.cid}'), - channel: channel, - child: Slidable( - enabled: widget.swipeToAction, - endActionPane: ActionPane( - extentRatio: actionPaneExtentRatio, - motion: const BehindMotion(), - children: widget.swipeActions - ?.map((e) => CustomSlidableAction( - backgroundColor: e.color ?? Colors.white, - child: e.iconWidget, - onPressed: (_) { - e.onTap?.call(channel); - }, - )) - .toList() ?? - [ - CustomSlidableAction( - backgroundColor: backgroundColor, - onPressed: widget.onMoreDetailsPressed != null - ? (_) { - widget.onMoreDetailsPressed!(channel); - } - : (_) { - showModalBottomSheet( - clipBehavior: Clip.hardEdge, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(32), - topRight: Radius.circular(32), - ), - ), - context: context, - builder: (context) => StreamChannel( - channel: channel, - child: StreamChannelInfoBottomSheet( - channel: channel, - onViewInfoTap: () { - widget.onViewInfoTap?.call(channel); - }, - ), - ), - ); - }, - child: const Icon(Icons.more_horiz), - ), - if (canDeleteChannel) - CustomSlidableAction( - backgroundColor: backgroundColor, - onPressed: widget.onDeletePressed != null - ? (_) { - widget.onDeletePressed?.call(channel); - } - : (_) async { - final res = await showConfirmationDialog( - context, - title: - context.translations.deleteConversationLabel, - question: context - .translations.deleteConversationQuestion, - okText: context.translations.deleteLabel, - cancelText: context.translations.cancelLabel, - icon: StreamSvgIcon.delete( - color: chatThemeData.colorTheme.accentError, - ), - ); - if (res == true) { - await channel.delete(); - } - }, - child: StreamSvgIcon.delete( - color: chatThemeData.colorTheme.accentError, - ), - ), - ], - ), - child: widget.channelPreviewBuilder?.call(context, channel) ?? - DecoratedBox( - decoration: BoxDecoration( - color: chatThemeData.channelListViewTheme.backgroundColor, - ), - child: ChannelPreview( - onLongPress: widget.onChannelLongPress, - channel: channel, - onImageTap: widget.onImageTap != null - ? () => widget.onImageTap!(channel) - : null, - onTap: (channel) => onTap(channel, widget.channelWidget), - ), - ), - ), - ); - } - - ChannelTapCallback _getChannelTap(BuildContext context) { - ChannelTapCallback onTap; - if (widget.onChannelTap != null) { - onTap = widget.onChannelTap!; - } else { - onTap = (channel, _) { - if (widget.channelWidget == null) { - return; - } - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => StreamChannel( - channel: channel, - child: widget.channelWidget!, - ), - ), - ); - }; - } - return onTap; - } - - Widget _gridItemBuilder(BuildContext context, int i, List channels) { - final channel = channels[i]; - - final selected = widget.selectedChannels.contains(channel); - - return Container( - key: ValueKey('CHANNEL-${channel.id}'), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - StreamChannelAvatar( - channel: channel, - borderRadius: BorderRadius.circular(32), - selected: selected, - constraints: const BoxConstraints.tightFor( - width: 64, - height: 64, - ), - onTap: () => _getChannelTap(context), - ), - const SizedBox(height: 7), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: StreamChannel( - channel: channel, - child: StreamChannelName( - channel: channel, - textStyle: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ], - ), - ); - } - - Widget _buildQueryProgressIndicator( - context, - ChannelsBlocState channelsProvider, - ) => - BetterStreamBuilder( - stream: channelsProvider.queryChannelsLoading, - initialData: false, - errorBuilder: (context, err) { - final theme = StreamChatTheme.of(context); - return ColoredBox( - color: theme.colorTheme.textLowEmphasis.withOpacity(0.9), - child: Padding( - padding: const EdgeInsets.all(16), - child: Text( - context.translations.loadingChannelsError, - style: theme.textTheme.body.copyWith( - color: Colors.white, - ), - ), - ), - ); - }, - builder: (context, showLoading) { - if (!showLoading) return const Offstage(); - return const Center( - child: Padding( - padding: EdgeInsets.all(16), - child: CircularProgressIndicator(), - ), - ); - }, - ); - - Widget _separatorBuilder(context, i) { - final effect = StreamChatTheme.of(context).colorTheme.borderBottom; - - return Container( - height: 1, - color: effect.color!.withOpacity(effect.alpha ?? 1.0), - ); - } -} - -/// Class for slidable action -class SwipeAction { - /// Constructor for creating [SwipeAction] - SwipeAction({ - this.color, - required this.iconWidget, - this.onTap, - }); - - /// Background color of action - Color? color; - - /// Widget to display as icon - Widget iconWidget; - - /// Callback when icon is tapped - ChannelInfoCallback? onTap; -} diff --git a/packages/stream_chat_flutter/lib/src/channel_name.dart b/packages/stream_chat_flutter/lib/src/channel_name.dart deleted file mode 100644 index 546524c3..00000000 --- a/packages/stream_chat_flutter/lib/src/channel_name.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// It shows the current [Channel] name using a [Text] widget. -/// -/// The widget uses a [StreamBuilder] to render the channel information -/// image as soon as it updates. -@Deprecated("Use 'StreamChannelName' instead") -class ChannelName extends StatelessWidget { - /// Instantiate a new ChannelName - const ChannelName({ - super.key, - this.textStyle, - this.textOverflow = TextOverflow.ellipsis, - }); - - /// The style of the text displayed - final TextStyle? textStyle; - - /// How visual overflow should be handled. - final TextOverflow textOverflow; - - @override - Widget build(BuildContext context) { - final client = StreamChat.of(context); - final channel = StreamChannel.of(context).channel; - - assert(channel.state != null, 'Channel ${channel.id} is not initialized'); - - return BetterStreamBuilder( - stream: channel.nameStream, - initialData: channel.name, - builder: (context, channelName) => Text( - channelName, - style: textStyle, - overflow: textOverflow, - ), - noDataBuilder: (context) => _generateName( - client.currentUser!, - channel.state!.members, - ), - ); - } - - Widget _generateName( - User currentUser, - List members, - ) => - LayoutBuilder( - builder: (context, constraints) { - var channelName = context.translations.noTitleText; - final otherMembers = members.where( - (member) => member.userId != currentUser.id, - ); - - if (otherMembers.isNotEmpty) { - if (otherMembers.length == 1) { - final user = otherMembers.first.user; - if (user != null) { - channelName = user.name; - } - } else { - final maxWidth = constraints.maxWidth; - final maxChars = maxWidth / (textStyle?.fontSize ?? 1); - var currentChars = 0; - final currentMembers = []; - otherMembers.forEach((element) { - final newLength = - currentChars + (element.user?.name.length ?? 0); - if (newLength < maxChars) { - currentChars = newLength; - currentMembers.add(element); - } - }); - - final exceedingMembers = - otherMembers.length - currentMembers.length; - channelName = - '${currentMembers.map((e) => e.user?.name).join(', ')} ' - '${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; - } - } - - return Text( - channelName, - style: textStyle, - overflow: textOverflow, - ); - }, - ); -} diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart deleted file mode 100644 index b84979cb..00000000 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ /dev/null @@ -1,315 +0,0 @@ -import 'package:collection/collection.dart' - show IterableExtension, ListEquality; -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template channel_preview} -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_preview.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_preview_paint.png) -/// -/// It shows the current [Channel] preview. -/// -/// The widget uses a [StreamBuilder] to render the channel information -/// image as soon as it updates. -/// -/// Usually you don't use this widget as it's the default channel preview -/// used by [StreamChannelListView]. -/// -/// The widget renders the ui based on the first ancestor of type -/// [StreamChatTheme]. -/// Modify it to change the widget appearance. -/// {@endtemplate} -@Deprecated("Use 'StreamChannelListTile' instead") -class ChannelPreview extends StatelessWidget { - /// Constructor for creating [ChannelPreview] - const ChannelPreview({ - required this.channel, - super.key, - this.onTap, - this.onLongPress, - this.onImageTap, - this.title, - this.subtitle, - this.leading, - this.sendingIndicator, - this.trailing, - }); - - /// Function called when tapping this widget - final void Function(Channel)? onTap; - - /// Function called when long pressing this widget - final void Function(Channel)? onLongPress; - - /// Channel displayed - final Channel channel; - - /// The function called when the image is tapped - final VoidCallback? onImageTap; - - /// Widget rendering the title - final Widget? title; - - /// Widget rendering the subtitle - final Widget? subtitle; - - /// Widget rendering the leading element, by default - /// it shows the [StreamChannelAvatar] - final Widget? leading; - - /// Widget rendering the trailing element, - /// by default it shows the last message date - final Widget? trailing; - - /// Widget rendering the sending indicator, - /// by default it uses the [StreamSendingIndicator] widget - final Widget? sendingIndicator; - - @override - Widget build(BuildContext context) { - final channelPreviewTheme = ChannelPreviewTheme.of(context); - final streamChatState = StreamChat.of(context); - return BetterStreamBuilder( - stream: channel.isMutedStream, - initialData: channel.isMuted, - builder: (context, data) => AnimatedOpacity( - opacity: data ? 0.5 : 1, - duration: const Duration(milliseconds: 300), - child: ListTile( - visualDensity: VisualDensity.compact, - contentPadding: const EdgeInsets.symmetric( - horizontal: 8, - ), - onTap: () => onTap?.call(channel), - onLongPress: () => onLongPress?.call(channel), - leading: leading ?? - StreamChannelAvatar( - channel: channel, - onTap: onImageTap, - ), - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: title ?? - StreamChannelName( - channel: channel, - textStyle: channelPreviewTheme.titleStyle, - ), - ), - BetterStreamBuilder>( - stream: channel.state?.membersStream, - initialData: channel.state?.members, - comparator: const ListEquality().equals, - builder: (context, members) { - if (members.isEmpty || - !members.any((Member e) => - e.user!.id == channel.client.state.currentUser?.id)) { - return const SizedBox(); - } - return StreamUnreadIndicator( - cid: channel.cid, - ); - }, - ), - ], - ), - subtitle: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible(child: subtitle ?? _buildSubtitle(context)), - sendingIndicator ?? - Builder( - builder: (context) { - final lastMessage = - channel.state?.messages.lastWhereOrNull( - (m) => !m.isDeleted && !m.shadowed, - ); - if (lastMessage?.user?.id == - streamChatState.currentUser?.id) { - return Padding( - padding: const EdgeInsets.only(right: 4), - child: BetterStreamBuilder>( - stream: channel.state?.readStream, - initialData: channel.state?.read, - builder: (context, data) { - final readList = data.where((it) => - it.user.id != - channel.client.state.currentUser?.id && - (it.lastRead - .isAfter(lastMessage!.createdAt) || - it.lastRead.isAtSameMomentAs( - lastMessage.createdAt, - ))); - final isMessageRead = readList.length >= - (channel.memberCount ?? 0) - 1; - return StreamSendingIndicator( - message: lastMessage!, - size: channelPreviewTheme.indicatorIconSize, - isMessageRead: isMessageRead, - ); - }, - ), - ); - } - return const SizedBox(); - }, - ), - trailing ?? _buildDate(context), - ], - ), - ), - ), - ); - } - - Widget _buildDate(BuildContext context) => BetterStreamBuilder( - stream: channel.lastMessageAtStream, - initialData: channel.lastMessageAt, - builder: (context, data) { - final lastMessageAt = data.toLocal(); - - String stringDate; - final now = DateTime.now(); - - final startOfDay = DateTime(now.year, now.month, now.day); - - if (lastMessageAt.millisecondsSinceEpoch >= - startOfDay.millisecondsSinceEpoch) { - stringDate = Jiffy(lastMessageAt.toLocal()).jm; - } else if (lastMessageAt.millisecondsSinceEpoch >= - startOfDay - .subtract(const Duration(days: 1)) - .millisecondsSinceEpoch) { - stringDate = context.translations.yesterdayLabel; - } else if (startOfDay.difference(lastMessageAt).inDays < 7) { - stringDate = Jiffy(lastMessageAt.toLocal()).EEEE; - } else { - stringDate = Jiffy(lastMessageAt.toLocal()).yMd; - } - - return Text( - stringDate, - style: ChannelPreviewTheme.of(context).lastMessageAtStyle, - ); - }, - ); - - Widget _buildSubtitle(BuildContext context) { - final channelPreviewTheme = ChannelPreviewTheme.of(context); - if (channel.isMuted) { - return Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - StreamSvgIcon.mute( - size: 16, - ), - Text( - ' ${context.translations.channelIsMutedText}', - style: channelPreviewTheme.subtitleStyle, - ), - ], - ); - } - return StreamTypingIndicator( - channel: channel, - alternativeWidget: _buildLastMessage(context), - style: channelPreviewTheme.subtitleStyle, - ); - } - - Widget _buildLastMessage(BuildContext context) => Align( - alignment: Alignment.centerLeft, - child: BetterStreamBuilder>( - stream: channel.state!.messagesStream, - initialData: channel.state!.messages, - builder: (context, data) { - final lastMessage = - data.lastWhereOrNull((m) => !m.shadowed && !m.isDeleted); - if (lastMessage == null) { - return const SizedBox(); - } - - var text = lastMessage.text; - final parts = [ - ...lastMessage.attachments.map((e) { - if (e.type == 'image') { - return '📷'; - } else if (e.type == 'video') { - return '🎬'; - } else if (e.type == 'giphy') { - return '[GIF]'; - } - return e == lastMessage.attachments.last - ? (e.title ?? 'File') - : '${e.title ?? 'File'} , '; - }), - lastMessage.text ?? '', - ]; - - text = parts.join(' '); - - final channelPreviewTheme = ChannelPreviewTheme.of(context); - return Text.rich( - _getDisplayText( - text, - lastMessage.mentionedUsers, - lastMessage.attachments, - channelPreviewTheme.subtitleStyle?.copyWith( - color: channelPreviewTheme.subtitleStyle?.color, - fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) - ? FontStyle.italic - : FontStyle.normal, - ), - channelPreviewTheme.subtitleStyle?.copyWith( - color: channelPreviewTheme.subtitleStyle?.color, - fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) - ? FontStyle.italic - : FontStyle.normal, - fontWeight: FontWeight.bold, - ), - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.start, - ); - }, - ), - ); - - TextSpan _getDisplayText( - String text, - List mentions, - List attachments, - TextStyle? normalTextStyle, - TextStyle? mentionsTextStyle, - ) { - final textList = text.split(' '); - final resList = []; - for (final e in textList) { - if (mentions.isNotEmpty && - mentions.any((element) => '@${element.name}' == e)) { - resList.add(TextSpan( - text: '$e ', - style: mentionsTextStyle, - )); - } else if (attachments.isNotEmpty && - attachments - .where((e) => e.title != null) - .any((element) => element.title == e)) { - resList.add(TextSpan( - text: '$e ', - style: normalTextStyle?.copyWith(fontStyle: FontStyle.italic), - )); - } else { - resList.add(TextSpan( - text: e == textList.last ? e : '$e ', - style: normalTextStyle, - )); - } - } - - return TextSpan(children: resList); - } -} diff --git a/packages/stream_chat_flutter/lib/src/commands_overlay.dart b/packages/stream_chat_flutter/lib/src/commands_overlay.dart deleted file mode 100644 index 64121e31..00000000 --- a/packages/stream_chat_flutter/lib/src/commands_overlay.dart +++ /dev/null @@ -1,221 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@macro commands_overlay} -@Deprecated("Use 'StreamCommandsOverlay' instead") -typedef CommandsOverlay = StreamCommandsOverlay; - -/// {@template commands_overlay} -/// Overlay for displaying commands that can be used -/// to interact with the channel. -/// {@endtemplate} -class StreamCommandsOverlay extends StatelessWidget { - /// Constructor for creating a [StreamCommandsOverlay] - const StreamCommandsOverlay({ - required this.text, - required this.onCommandResult, - required this.size, - required this.channel, - super.key, - }); - - /// The size of the overlay - final Size size; - - /// Query for searching commands - final String text; - - /// The channel to search for users - final Channel channel; - - /// Callback called when a command is selected - final ValueChanged onCommandResult; - - @override - Widget build(BuildContext context) { - final _streamChatTheme = StreamChatTheme.of(context); - final commands = channel.config?.commands - .where((c) => c.name.contains(text.replaceFirst('/', ''))) - .toList() ?? - []; - - if (commands.isEmpty) { - return const SizedBox(); - } - - return Padding( - padding: const EdgeInsets.all(4), - child: Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - color: _streamChatTheme.colorTheme.barsBg, - clipBehavior: Clip.hardEdge, - child: Container( - constraints: BoxConstraints.loose(size), - decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.barsBg, - borderRadius: BorderRadius.circular(8), - ), - child: ListView( - padding: EdgeInsets.zero, - shrinkWrap: true, - children: [ - if (commands.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 8), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - ), - child: StreamSvgIcon.lightning( - color: _streamChatTheme.colorTheme.accentPrimary, - ), - ), - Text( - context.translations.instantCommandsLabel, - style: TextStyle( - color: _streamChatTheme.colorTheme.textHighEmphasis - .withOpacity(0.5), - ), - ), - ], - ), - ), - const SizedBox( - height: 10, - ), - ...commands - .map( - (c) => InkWell( - onTap: () { - onCommandResult(c); - }, - child: SizedBox( - height: 40, - child: Row( - children: [ - const SizedBox( - width: 16, - ), - _buildCommandIcon(_streamChatTheme, c.name), - const SizedBox( - width: 8, - ), - Text.rich( - TextSpan( - text: c.name.capitalize(), - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - children: [ - TextSpan( - text: ' /${c.name} ${c.args}', - style: _streamChatTheme.textTheme.body - .copyWith( - // ignore: lines_longer_than_80_chars - color: _streamChatTheme - // ignore: lines_longer_than_80_chars - .colorTheme - .textLowEmphasis, - ), - ), - ], - ), - ), - ], - ), - ), - ), - ) - .toList(), - ], - ), - ), - ), - ); - } - - Widget _buildCommandIcon( - StreamChatThemeData _streamChatTheme, - String iconType, - ) { - switch (iconType) { - case 'giphy': - return CircleAvatar( - radius: 12, - child: StreamSvgIcon.giphyIcon( - size: 24, - ), - ); - case 'ban': - return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentPrimary, - radius: 12, - child: StreamSvgIcon.iconUserDelete( - size: 16, - color: Colors.white, - ), - ); - case 'flag': - return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentPrimary, - radius: 12, - child: StreamSvgIcon.flag( - size: 14, - color: Colors.white, - ), - ); - case 'imgur': - return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentPrimary, - radius: 12, - child: ClipOval( - child: StreamSvgIcon.imgur( - size: 24, - ), - ), - ); - case 'mute': - return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentPrimary, - radius: 12, - child: StreamSvgIcon.mute( - size: 16, - color: Colors.white, - ), - ); - case 'unban': - return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentPrimary, - radius: 12, - child: StreamSvgIcon.userAdd( - size: 16, - color: Colors.white, - ), - ); - case 'unmute': - return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentPrimary, - radius: 12, - child: StreamSvgIcon.volumeUp( - size: 16, - color: Colors.white, - ), - ); - default: - return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentPrimary, - radius: 12, - child: StreamSvgIcon.lightning( - size: 16, - color: Colors.white, - ), - ); - } - } -} diff --git a/packages/stream_chat_flutter/lib/src/context_menu_items/context_menu_reaction_picker.dart b/packages/stream_chat_flutter/lib/src/context_menu_items/context_menu_reaction_picker.dart new file mode 100644 index 00000000..184a55de --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/context_menu_items/context_menu_reaction_picker.dart @@ -0,0 +1,176 @@ +import 'package:ezanimation/ezanimation.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template contextMenuReactionPicker} +/// Allows the user to select reactions to a message on desktop & web via +/// context menu. +/// +/// This differs slightly from [StreamReactionPicker] in order to match our +/// design spec. +/// +/// Used by the `_buildContextMenu()` function found in `message_widget.dart`. +/// It is not recommended to use this widget directly. +/// {@endtemplate} +class ContextMenuReactionPicker extends StatefulWidget { + /// {@macro contextMenuReactionPicker} + const ContextMenuReactionPicker({ + super.key, + required this.message, + }); + + /// The message to react to. + final Message message; + + @override + State createState() => + _ContextMenuReactionPickerState(); +} + +class _ContextMenuReactionPickerState extends State + with TickerProviderStateMixin { + List animations = []; + + Future triggerAnimations() async { + for (final a in animations) { + a.start(); + await Future.delayed(const Duration(milliseconds: 100)); + } + } + + Future pop() async { + for (final a in animations) { + a.stop(); + } + Navigator.of(context).pop(); + } + + /// Add a reaction to the message + void sendReaction(BuildContext context, String reactionType) { + StreamChannel.of(context).channel.sendReaction( + widget.message, + reactionType, + enforceUnique: + StreamChatConfiguration.of(context).enforceUniqueReactions, + ); + pop(); + } + + /// Remove a reaction from the message + void removeReaction(BuildContext context, Reaction reaction) { + StreamChannel.of(context).channel.deleteReaction(widget.message, reaction); + pop(); + } + + @override + void dispose() { + for (final a in animations) { + a.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final reactionIcons = StreamChatConfiguration.of(context).reactionIcons; + + if (animations.isEmpty && reactionIcons.isNotEmpty) { + reactionIcons.forEach((element) { + animations.add( + EzAnimation.tween( + Tween(begin: 0.0, end: 1.0), + const Duration(milliseconds: 250), + curve: Curves.easeInOutBack, + ), + ); + }); + + triggerAnimations(); + } + + final child = Material( + color: StreamChatTheme.of(context).messageListViewTheme.backgroundColor ?? + Theme.of(context).scaffoldBackgroundColor, + //clipBehavior: Clip.hardEdge, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceAround, + mainAxisSize: MainAxisSize.min, + children: reactionIcons + .map((reactionIcon) { + final ownReactionIndex = + widget.message.ownReactions?.indexWhere( + (reaction) => reaction.type == reactionIcon.type, + ) ?? + -1; + final index = reactionIcons.indexOf(reactionIcon); + + final child = reactionIcon.builder( + context, + ownReactionIndex != -1, + 24, + ); + + return ConstrainedBox( + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, + ), + child: RawMaterialButton( + elevation: 0, + shape: ContinuousRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, + ), + onPressed: () { + if (ownReactionIndex != -1) { + removeReaction( + context, + widget.message.ownReactions![ownReactionIndex], + ); + } else { + sendReaction( + context, + reactionIcon.type, + ); + } + }, + child: AnimatedBuilder( + animation: animations[index], + builder: (context, child) => Transform.scale( + scale: animations[index].value, + child: child, + ), + child: child, + ), + ), + ); + }) + .insertBetween( + const SizedBox( + width: 16, + ), + ) + .toList(), + ), + ), + ); + + return TweenAnimationBuilder( + tween: Tween(begin: 0, end: 1), + curve: Curves.easeInOutBack, + duration: const Duration(milliseconds: 500), + builder: (context, val, widget) => Transform.scale( + scale: val, + child: widget, + ), + child: child, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/context_menu_items/download_menu_item.dart b/packages/stream_chat_flutter/lib/src/context_menu_items/download_menu_item.dart new file mode 100644 index 00000000..57d2b5af --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/context_menu_items/download_menu_item.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/context_menu_items/stream_chat_context_menu_item.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template downloadMenuItem} +/// Defines a "download" context menu item that allows a user to download +/// a given attachment. +/// +/// Used in [DesktopFullscreenMedia]. +/// {@endtemplate} +class DownloadMenuItem extends StatelessWidget { + /// {@macro downloadMenuItem} + const DownloadMenuItem({ + super.key, + required this.attachment, + }); + + /// The attachment to download. + final Attachment attachment; + + @override + Widget build(BuildContext context) { + return StreamChatContextMenuItem( + leading: StreamSvgIcon.download(), + title: Text(context.translations.downloadLabel), + onClick: () async { + Navigator.of(context).pop(); + StreamAttachmentHandler.instance.downloadAttachment(attachment); + }, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/context_menu_items/stream_chat_context_menu_item.dart b/packages/stream_chat_flutter/lib/src/context_menu_items/stream_chat_context_menu_item.dart new file mode 100644 index 00000000..dfe64684 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/context_menu_items/stream_chat_context_menu_item.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template streamChatContextMenuItem} +/// Builds a context menu item according to Stream design specification. +/// {@endtemplate} +class StreamChatContextMenuItem extends StatelessWidget { + /// {@macro streamChatContextMenuItem} + const StreamChatContextMenuItem({ + super.key, + this.child, + this.leading, + this.title, + this.onClick, + }); + + /// The child widget for this menu item. Usually a [DesktopReactionPicker]. + /// + /// Leave null in order to use the default menu item widget. + final Widget? child; + + /// The widget to lead the menu item with. Usually an [Icon]. + /// + /// If [child] is specified, this will be ignored. + final Widget? leading; + + /// The title of the menu item. Usually a [Text]. + /// + /// If [child] is specified, this will be ignored. + final Widget? title; + + /// The action to perform when the menu item is clicked. + /// + /// If [child] is specified, this will be ignored. + final VoidCallback? onClick; + + @override + Widget build(BuildContext context) { + return Ink( + color: StreamChatTheme.of(context).messageListViewTheme.backgroundColor ?? + Theme.of(context).scaffoldBackgroundColor, + child: child ?? + ListTile( + dense: true, + leading: leading, + title: title, + onTap: onClick, + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/dialogs/channel_info_dialog.dart b/packages/stream_chat_flutter/lib/src/dialogs/channel_info_dialog.dart new file mode 100644 index 00000000..9d997c58 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/dialogs/channel_info_dialog.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template channelInfoDialog} +/// A dialog for showing information about a channel on desktop & web platforms. +/// {@endtemplate} +class ChannelInfoDialog extends StatelessWidget { + /// {@macro channelInfoDialog} + const ChannelInfoDialog({ + super.key, + required this.channel, + }); + + /// The channel to display information about. + final Channel channel; + + @override + Widget build(BuildContext context) { + final streamTheme = StreamChatTheme.of(context); + final members = channel.state?.members ?? []; + + final userAsMember = members.firstWhere( + (e) => e.user?.id == StreamChat.of(context).currentUser?.id, + ); + return StreamChannel( + channel: channel, + child: SimpleDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + backgroundColor: streamTheme.colorTheme.appBg, + title: Text( + channel.name ?? channel.id!, + style: StreamChatTheme.of(context).textTheme.headlineBold, + ), + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + StreamChannelInfo( + channel: channel, + textStyle: StreamChatTheme.of(context) + .channelPreviewTheme + .subtitleStyle, + ), + ], + ), + const SizedBox(height: 16), + if (channel.isDistinct && channel.memberCount == 2) + Column( + children: [ + StreamUserAvatar( + user: members + .firstWhere( + (e) => e.user?.id != userAsMember.user?.id, + ) + .user!, + constraints: const BoxConstraints( + maxHeight: 64, + maxWidth: 64, + ), + borderRadius: BorderRadius.circular(32), + onlineIndicatorConstraints: + BoxConstraints.tight(const Size(12, 12)), + ), + const SizedBox(height: 6), + Text( + members + .firstWhere( + (e) => e.user?.id != userAsMember.user?.id, + ) + .user + ?.name ?? + '', + style: StreamChatTheme.of(context).textTheme.footnoteBold, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ], + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/dialogs/confirmation_dialog.dart b/packages/stream_chat_flutter/lib/src/dialogs/confirmation_dialog.dart new file mode 100644 index 00000000..b4b089e1 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/dialogs/confirmation_dialog.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// /// {@template confirmationDialog} +/// A dialog that prompts the user to take an action or cancel. +/// {@endtemplate} +class ConfirmationDialog extends StatelessWidget { + /// {@macro confirmationDialog} + const ConfirmationDialog({ + super.key, + required this.titleText, + required this.promptText, + required this.affirmativeText, + required this.onConfirmation, + }); + + /// The text to use for the dialog title. + final String titleText; + + /// The text to use for the dialog prompt. + final String promptText; + + /// The text to use for the confirmation button. + final String affirmativeText; + + /// The action to perform when the user confirms their choice. + final VoidCallback onConfirmation; + + @override + Widget build(BuildContext context) { + final streamTheme = StreamChatTheme.of(context); + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + backgroundColor: streamTheme.colorTheme.appBg, + title: Text(titleText), + content: Text(promptText), + actions: [ + TextButton( + style: TextButton.styleFrom( + foregroundColor: streamTheme.colorTheme.accentPrimary, + ), + onPressed: () => Navigator.of(context).pop(false), + child: Text(context.translations.cancelLabel), + ), + TextButton( + style: TextButton.styleFrom( + foregroundColor: streamTheme.colorTheme.accentPrimary, + ), + onPressed: () { + onConfirmation.call(); + Navigator.of(context).pop(true); + }, + child: Text(affirmativeText), + ), + ], + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/dialogs/delete_message_dialog.dart b/packages/stream_chat_flutter/lib/src/dialogs/delete_message_dialog.dart new file mode 100644 index 00000000..8612ea79 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/dialogs/delete_message_dialog.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template deleteMessageDialog} +/// A dialog that asks the user to confirm that they want to +/// delete the selected message. +/// {@endtemplate} +class DeleteMessageDialog extends StatelessWidget { + /// {@macro deleteMessageDialog} + const DeleteMessageDialog({ + super.key, + }); + + @override + Widget build(BuildContext context) { + final streamTheme = StreamChatTheme.of(context); + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + backgroundColor: streamTheme.colorTheme.appBg, + title: Text(context.translations.deleteMessageLabel), + content: Text(context.translations.deleteMessageQuestion), + actions: [ + TextButton( + style: TextButton.styleFrom( + foregroundColor: streamTheme.colorTheme.accentPrimary, + ), + onPressed: () => Navigator.of(context).pop(false), + child: Text(context.translations.cancelLabel), + ), + TextButton( + style: TextButton.styleFrom( + foregroundColor: streamTheme.colorTheme.accentPrimary, + ), + onPressed: () => Navigator.of(context).pop(true), + child: Text(context.translations.deleteLabel), + ), + ], + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/dialogs/dialogs.dart b/packages/stream_chat_flutter/lib/src/dialogs/dialogs.dart new file mode 100644 index 00000000..2daa1f6f --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/dialogs/dialogs.dart @@ -0,0 +1,4 @@ +export 'channel_info_dialog.dart'; +export 'confirmation_dialog.dart'; +export 'delete_message_dialog.dart'; +export 'message_dialog.dart'; diff --git a/packages/stream_chat_flutter/lib/src/dialogs/message_dialog.dart b/packages/stream_chat_flutter/lib/src/dialogs/message_dialog.dart new file mode 100644 index 00000000..0f4fce0d --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/dialogs/message_dialog.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template messageDialog} +/// A dialog that displays a message to a user. Falls back to a +/// generic error message if no [titleText] and [messageText] are specified. +/// +/// If using this dialog to display the default generic error, be sure NOT to +/// specify a [titleText] and [messageText] so the fallback strings can be used. +/// {@endtemplate} +class MessageDialog extends StatelessWidget { + /// {@macro messageDialog} + const MessageDialog({ + super.key, + this.titleText, + this.messageText, + }); + + /// The optional error message title to use. + final String? titleText; + + /// The optional error message to use. + final String? messageText; + + @override + Widget build(BuildContext context) { + final streamTheme = StreamChatTheme.of(context); + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + backgroundColor: streamTheme.colorTheme.appBg, + title: Text(titleText ?? context.translations.somethingWentWrongError), + content: messageText != null + ? Text( + messageText ?? + context.translations.operationCouldNotBeCompletedText, + ) + : null, + actions: [ + TextButton( + style: TextButton.styleFrom( + foregroundColor: streamTheme.colorTheme.accentPrimary, + ), + child: Text(context.translations.okLabel), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/emoji/emoji.dart b/packages/stream_chat_flutter/lib/src/emoji/emoji.dart deleted file mode 100644 index 36717000..00000000 --- a/packages/stream_chat_flutter/lib/src/emoji/emoji.dart +++ /dev/null @@ -1,114494 +0,0 @@ -// Copyright 2020 Naji. All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Naji nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import 'package:collection/collection.dart' - show IterableExtension, ListEquality; - -/// All Groups -enum EmojiGroup { - smileysEmotion, - activities, - peopleBody, - objects, - travelPlaces, - component, - animalsNature, - foodDrink, - symbols, - flags -} - -/// All Subgroups -enum EmojiSubgroup { - faceSmiling, - faceAffection, - faceSleepy, - faceTongue, - faceNeutralSkeptical, - faceGlasses, - faceHat, - faceConcerned, - faceNegative, - faceUnwell, - faceHand, - faceCostume, - event, - catFace, - hands, - handFingersClosed, - handFingersPartial, - handSingleFinger, - handFingersOpen, - bodyParts, - handProp, - clothing, - emotion, - personSymbol, - person, - personRole, - personFantasy, - personGesture, - personActivity, - family, - artsCrafts, - office, - hotel, - skyWeather, - hairStyle, - animalMammal, - animalAmphibian, - monkeyFace, - animalBird, - animalBug, - animalReptile, - animalMarine, - foodMarine, - plantOther, - foodVegetable, - placeBuilding, - plantFlower, - placeMap, - foodFruit, - foodAsian, - foodPrepared, - foodSweet, - drink, - dishware, - sport, - tool, - game, - transportGround, - personSport, - transportAir, - personResting, - awardMedal, - placeOther, - lightVideo, - music, - musicalInstrument, - transportWater, - otherObject, - placeGeographic, - placeReligious, - time, - phone, - computer, - science, - household, - money, - medical, - transportSign, - lock, - mail, - bookPaper, - sound, - writing, - religion, - zodiac, - alphanum, - warning, - avSymbol, - otherSymbol, - punctuation, - geometric, - keycap, - arrow, - math, - currency, - gender, - flag, - countryFlag, - subdivisionFlag, - skinTone, - regional -} - -/// List of All Emojis. -final List _emojis = [ - Emoji( - name: 'grinning face', - char: '\u{1F600}', - shortName: 'grinning', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceSmiling, - keywords: [ - 'face', - 'grin', - 'uc6', - 'smiley', - 'happy', - 'silly', - 'laugh', - 'thank you', - 'awesome', - 'smile', - 'friend', - 'pleased', - 'teeth', - 'pacman', - 'fun', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'funny', - 'laughing', - 'lol', - 'rofl', - 'lmao', - 'lmfao', - 'hilarious', - 'ha', - 'haha', - 'chuckle', - 'comedy', - 'giggle', - 'hehe', - 'joyful', - 'laugh out loud', - 'rire', - 'tee hee', - 'jaja', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'smiles', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'please', - 'chill', - 'confident', - 'content', - 'dentist', - 'pac man' - ]), - Emoji( - name: 'grinning face with big eyes', - char: '\u{1F603}', - shortName: 'smiley', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceSmiling, - keywords: [ - 'face', - 'mouth', - 'open', - 'smile', - 'uc6', - 'smiley', - 'happy', - 'silly', - 'laugh', - 'good', - 'smile', - 'teeth', - 'fun', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'funny', - 'laughing', - 'lol', - 'rofl', - 'lmao', - 'lmfao', - 'hilarious', - 'ha', - 'haha', - 'chuckle', - 'comedy', - 'giggle', - 'hehe', - 'joyful', - 'laugh out loud', - 'rire', - 'tee hee', - 'jaja', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'smiles', - 'dentist', - ':-D', - '=D' - ]), - Emoji( - name: 'grinning face with smiling eyes', - char: '\u{1F604}', - shortName: 'smile', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceSmiling, - keywords: [ - 'eye', - 'face', - 'mouth', - 'open', - 'smile', - 'uc6', - 'smiley', - 'happy', - 'laugh', - 'smile', - 'teeth', - 'fun', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'laughing', - 'lol', - 'rofl', - 'lmao', - 'lmfao', - 'hilarious', - 'ha', - 'haha', - 'chuckle', - 'comedy', - 'giggle', - 'hehe', - 'joyful', - 'laugh out loud', - 'rire', - 'tee hee', - 'jaja', - 'smiles', - 'dentist', - ':D' - ]), - Emoji( - name: 'beaming face with smiling eyes', - char: '\u{1F601}', - shortName: 'grin', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceSmiling, - keywords: [ - 'eye', - 'face', - 'grin', - 'smile', - 'uc6', - 'smiley', - 'happy', - 'silly', - 'laugh', - 'thank you', - 'good', - 'beautiful', - 'selfie', - 'smile', - 'friend', - 'teeth', - 'dumb', - 'grimace', - 'fun', - 'proud', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'funny', - 'laughing', - 'lol', - 'rofl', - 'lmao', - 'lmfao', - 'hilarious', - 'ha', - 'haha', - 'chuckle', - 'comedy', - 'giggle', - 'hehe', - 'joyful', - 'laugh out loud', - 'rire', - 'tee hee', - 'jaja', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'smiles', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'dentist', - 'idiot', - 'ignorant', - 'stupid' - ]), - Emoji( - name: 'grinning squinting face', - char: '\u{1F606}', - shortName: 'laughing', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceSmiling, - keywords: [ - 'face', - 'laugh', - 'mouth', - 'open', - 'satisfied', - 'smile', - 'uc6', - 'smiley', - 'happy', - 'silly', - 'laugh', - 'smile', - 'teeth', - 'dumb', - 'fun', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'funny', - 'laughing', - 'lol', - 'rofl', - 'lmao', - 'lmfao', - 'hilarious', - 'ha', - 'haha', - 'chuckle', - 'comedy', - 'giggle', - 'hehe', - 'joyful', - 'laugh out loud', - 'rire', - 'tee hee', - 'jaja', - 'smiles', - 'dentist', - 'idiot', - 'ignorant', - 'stupid', - '>:)', - '>;)', - '>:-)', - '>=)' - ]), - Emoji( - name: 'grinning face with sweat', - char: '\u{1F605}', - shortName: 'sweat_smile', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceSmiling, - keywords: [ - 'cold', - 'face', - 'open', - 'smile', - 'sweat', - 'uc6', - 'smiley', - 'happy', - 'laugh', - 'sweat', - 'smile', - 'tease', - 'drip', - 'guilty', - 'porn', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'laughing', - 'lol', - 'rofl', - 'lmao', - 'lmfao', - 'hilarious', - 'ha', - 'haha', - 'chuckle', - 'comedy', - 'giggle', - 'hehe', - 'joyful', - 'laugh out loud', - 'rire', - 'tee hee', - 'jaja', - 'smiles', - 'joke', - 'kidding', - ':)', - ':-)', - '=)', - ':D', - ':-D', - '=D' - ]), - Emoji( - name: 'face with tears of joy', - char: '\u{1F602}', - shortName: 'joy', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceSmiling, - keywords: [ - 'face', - 'joy', - 'laugh', - 'tear', - 'uc6', - 'smiley', - 'happy', - 'silly', - 'cry', - 'laugh', - 'sarcastic', - 'smile', - 'tease', - 'crazy', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'funny', - 'crying', - 'weeping', - 'weep', - 'sob', - 'sobbing', - 'tear', - 'tears', - 'bawling', - 'laughing', - 'lol', - 'rofl', - 'lmao', - 'lmfao', - 'hilarious', - 'ha', - 'haha', - 'chuckle', - 'comedy', - 'giggle', - 'hehe', - 'joyful', - 'laugh out loud', - 'rire', - 'tee hee', - 'jaja', - 'sarcasm', - 'smiles', - 'joke', - 'kidding', - 'weird', - 'awkward', - 'insane', - 'wild', - ":')", - ":'-)" - ]), - Emoji( - name: 'rolling on the floor laughing', - char: '\u{1F923}', - shortName: 'rofl', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceSmiling, - keywords: [ - 'face', - 'floor', - 'laugh', - 'rolling', - 'uc9', - 'smiley', - 'happy', - 'silly', - 'laugh', - 'tease', - 'crazy', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'funny', - 'laughing', - 'lol', - 'rofl', - 'lmao', - 'lmfao', - 'hilarious', - 'ha', - 'haha', - 'chuckle', - 'comedy', - 'giggle', - 'hehe', - 'joyful', - 'laugh out loud', - 'rire', - 'tee hee', - 'jaja', - 'joke', - 'kidding', - 'weird', - 'awkward', - 'insane', - 'wild' - ]), - Emoji( - name: 'smiling face', - char: '\u{263A}\u{FE0F}', - shortName: 'relaxed', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceAffection, - keywords: [ - 'face', - 'outlined', - 'relaxed', - 'smile', - 'uc1', - 'smiley', - 'happy', - 'beautiful', - 'smile', - 'blush', - 'pleased', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'smiles', - 'blushing', - 'bella', - 'embarrassed', - 'creep', - 'please', - 'chill', - 'confident', - 'content' - ]), - Emoji( - name: 'smiling face with smiling eyes', - char: '\u{1F60A}', - shortName: 'blush', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceSmiling, - keywords: [ - 'blush', - 'eye', - 'face', - 'smile', - 'uc6', - 'smiley', - 'happy', - 'good', - 'beautiful', - 'smile', - 'blush', - 'pleased', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'smiles', - 'blushing', - 'bella', - 'embarrassed', - 'creep', - 'please', - 'chill', - 'confident', - 'content' - ]), - Emoji( - name: 'smiling face with halo', - char: '\u{1F607}', - shortName: 'innocent', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceSmiling, - keywords: [ - 'angel', - 'face', - 'fairy tale', - 'fantasy', - 'halo', - 'innocent', - 'smile', - 'uc6', - 'smiley', - 'silly', - 'pray', - 'smile', - 'blush', - 'fantasy', - 'soul', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'funny', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'smiles', - 'blushing', - 'bella', - 'embarrassed', - 'creep', - 'O:-)', - '0:-3', - '0:3', - '0:-)', - '0:)', - '0;^)', - 'O:)', - 'O;-)', - 'O=)', - '0;-)', - 'O:-3', - 'O:3' - ]), - Emoji( - name: 'slightly smiling face', - char: '\u{1F642}', - shortName: 'slight_smile', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceSmiling, - keywords: [ - 'face', - 'smile', - 'uc7', - 'smiley', - 'happy', - 'awesome', - 'smile', - 'blush', - 'pleased', - 'fun', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'smiles', - 'blushing', - 'bella', - 'embarrassed', - 'creep', - 'please', - 'chill', - 'confident', - 'content', - ':)', - ':-)', - '=]', - '=)', - ':]' - ]), - Emoji( - name: 'upside-down face', - char: '\u{1F643}', - shortName: 'upside_down', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceSmiling, - keywords: [ - 'face', - 'upside-down', - 'uc8', - 'smiley', - 'happy', - 'silly', - 'sarcastic', - 'smile', - 'pleased', - 'dumb', - 'what', - 'clever', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'funny', - 'sarcasm', - 'smiles', - 'please', - 'chill', - 'confident', - 'content', - 'idiot', - 'ignorant', - 'stupid', - 'witty' - ]), - Emoji( - name: 'winking face', - char: '\u{1F609}', - shortName: 'wink', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceSmiling, - keywords: [ - 'face', - 'wink', - 'uc6', - 'smiley', - 'happy', - 'silly', - 'sarcastic', - 'selfie', - 'smile', - 'tease', - 'clever', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'funny', - 'sarcasm', - 'smiles', - 'joke', - 'kidding', - 'witty', - ';)', - ';-)', - '*-)', - '*)', - ';-]', - ';]', - ';D', - ';^)' - ]), - Emoji( - name: 'relieved face', - char: '\u{1F60C}', - shortName: 'relieved', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceSleepy, - keywords: [ - 'face', - 'relieved', - 'uc6', - 'smiley', - 'happy', - 'smile', - 'pleased', - 'calm', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'smiles', - 'please', - 'chill', - 'confident', - 'content' - ]), - Emoji( - name: 'smiling face with tear', - char: '\u{1F972}', - shortName: 'smiling_face_with_tear', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceAffection, - keywords: [ - 'uc13', - 'smiley', - 'happy', - 'cry', - 'thank you', - 'beautiful', - 'smile', - 'blush', - 'pleased', - 'drip', - 'hope', - 'proud', - 'sentimental', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'crying', - 'weeping', - 'weep', - 'sob', - 'sobbing', - 'tear', - 'tears', - 'bawling', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'smiles', - 'blushing', - 'bella', - 'embarrassed', - 'creep', - 'please', - 'chill', - 'confident', - 'content', - 'swear', - 'promise', - 'nostalgic', - 'tender', - 'dreamy', - 'touched' - ]), - Emoji( - name: 'smiling face with heart-eyes', - char: '\u{1F60D}', - shortName: 'heart_eyes', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceAffection, - keywords: [ - 'eye', - 'face', - 'love', - 'smile', - 'uc6', - 'smiley', - 'happy', - 'love', - 'heart eyes', - 'beautiful', - 'smile', - 'hola', - 'facebook', - 'porn', - 'heart', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'smiles', - 'hearts', - 'serce', - 'corazón', - 'coração', - 'coeur' - ]), - Emoji( - name: 'smiling face with hearts', - char: '\u{1F970}', - shortName: 'smiling_face_with_3_hearts', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceAffection, - keywords: [ - 'uc11', - 'smiley', - 'wedding', - 'happy', - 'love', - 'hug', - 'smile', - 'friend', - 'blush', - 'pleased', - 'heart', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'embrace', - 'hugs', - 'smiles', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'blushing', - 'bella', - 'embarrassed', - 'creep', - 'please', - 'chill', - 'confident', - 'content', - 'hearts', - 'serce', - 'corazón', - 'coração', - 'coeur' - ]), - Emoji( - name: 'face blowing a kiss', - char: '\u{1F618}', - shortName: 'kissing_heart', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceAffection, - keywords: [ - 'face', - 'kiss', - 'uc6', - 'smiley', - 'wedding', - 'love', - 'sexy', - 'beautiful', - 'disney', - 'kisses', - 'hit', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'cartoon', - 'bisous', - 'beijos', - 'besos', - 'bise', - 'blowing kisses', - 'kissy', - 'punch', - 'pow', - 'bam', - ':*', - ':-*', - '=*', - ':^*' - ]), - Emoji( - name: 'kissing face', - char: '\u{1F617}', - shortName: 'kissing', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceAffection, - keywords: [ - 'face', - 'kiss', - 'uc6', - 'smiley', - 'sexy', - 'beautiful', - 'selfie', - 'kisses', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'bisous', - 'beijos', - 'besos', - 'bise', - 'blowing kisses', - 'kissy' - ]), - Emoji( - name: 'kissing face with smiling eyes', - char: '\u{1F619}', - shortName: 'kissing_smiling_eyes', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceAffection, - keywords: [ - 'eye', - 'face', - 'kiss', - 'smile', - 'uc6', - 'smiley', - 'love', - 'sexy', - 'kisses', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'bisous', - 'beijos', - 'besos', - 'bise', - 'blowing kisses', - 'kissy' - ]), - Emoji( - name: 'kissing face with closed eyes', - char: '\u{1F61A}', - shortName: 'kissing_closed_eyes', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceAffection, - keywords: [ - 'closed', - 'eye', - 'face', - 'kiss', - 'uc6', - 'smiley', - 'love', - 'sexy', - 'blush', - 'kisses', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'blushing', - 'bella', - 'embarrassed', - 'creep', - 'bisous', - 'beijos', - 'besos', - 'bise', - 'blowing kisses', - 'kissy' - ]), - Emoji( - name: 'face savoring food', - char: '\u{1F60B}', - shortName: 'yum', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceTongue, - keywords: [ - 'delicious', - 'face', - 'savouring', - 'smile', - 'um', - 'yum', - 'uc6', - 'smiley', - 'food', - 'happy', - 'silly', - 'sarcastic', - 'good', - 'smile', - 'pink', - 'lick', - 'tongue', - 'dinner', - 'picnic', - 'delicious', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'funny', - 'sarcasm', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'smiles', - 'rose', - 'toung', - 'tounge', - 'lunch', - 'savour' - ]), - Emoji( - name: 'face with tongue', - char: '\u{1F61B}', - shortName: 'stuck_out_tongue', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceTongue, - keywords: [ - 'face', - 'tongue', - 'uc6', - 'smiley', - 'happy', - 'silly', - 'pink', - 'tease', - 'lick', - 'tongue', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'funny', - 'rose', - 'joke', - 'kidding', - 'toung', - 'tounge', - ':P', - ':-P', - '=P', - ':-Þ', - ':Þ', - ':-b', - ':b' - ]), - Emoji( - name: 'squinting face with tongue', - char: '\u{1F61D}', - shortName: 'stuck_out_tongue_closed_eyes', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceTongue, - keywords: [ - 'eye', - 'face', - 'horrible', - 'taste', - 'tongue', - 'uc6', - 'smiley', - 'happy', - 'silly', - 'laugh', - 'pink', - 'tease', - 'grimace', - 'lick', - 'tongue', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'funny', - 'laughing', - 'lol', - 'rofl', - 'lmao', - 'lmfao', - 'hilarious', - 'ha', - 'haha', - 'chuckle', - 'comedy', - 'giggle', - 'hehe', - 'joyful', - 'laugh out loud', - 'rire', - 'tee hee', - 'jaja', - 'rose', - 'joke', - 'kidding', - 'toung', - 'tounge' - ]), - Emoji( - name: 'winking face with tongue', - char: '\u{1F61C}', - shortName: 'stuck_out_tongue_winking_eye', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceTongue, - keywords: [ - 'eye', - 'face', - 'joke', - 'tongue', - 'wink', - 'uc6', - 'smiley', - 'happy', - 'silly', - 'pink', - 'tease', - 'pleased', - 'lick', - 'porn', - 'crazy', - 'tongue', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'funny', - 'rose', - 'joke', - 'kidding', - 'please', - 'chill', - 'confident', - 'content', - 'weird', - 'awkward', - 'insane', - 'wild', - 'toung', - 'tounge', - '>:P', - 'X-P' - ]), - Emoji( - name: 'zany face', - char: '\u{1F92A}', - shortName: 'zany_face', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceTongue, - keywords: [ - 'eye', - 'large', - 'small', - 'uc10', - 'smiley', - 'silly', - 'nutcase', - 'crazy', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'funny', - 'weird', - 'awkward', - 'insane', - 'wild' - ]), - Emoji( - name: 'face with raised eyebrow', - char: '\u{1F928}', - shortName: 'face_with_raised_eyebrow', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceNeutralSkeptical, - keywords: [ - 'uc10', - 'smiley', - 'doubt', - 'jealous', - 'colbert', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical' - ]), - Emoji( - name: 'face with monocle', - char: '\u{1F9D0}', - shortName: 'face_with_monocle', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceGlasses, - keywords: [ - 'uc10', - 'smiley', - 'nerd', - 'rich', - 'mystery', - 'proud', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'smart', - 'geek', - 'serious', - 'grand', - 'expensive', - 'fancy' - ]), - Emoji( - name: 'nerd face', - char: '\u{1F913}', - shortName: 'nerd', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceGlasses, - keywords: [ - 'face', - 'geek', - 'nerd', - 'uc8', - 'smiley', - 'glasses', - 'nerd', - 'google', - 'brain', - 'teeth', - 'dumb', - 'disguise', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'eyeglasses', - 'eye glasses', - 'smart', - 'geek', - 'serious', - 'mind', - 'memory', - 'thought', - 'conscience', - 'dentist', - 'idiot', - 'ignorant', - 'stupid' - ]), - Emoji( - name: 'smiling face with sunglasses', - char: '\u{1F60E}', - shortName: 'sunglasses', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceGlasses, - keywords: [ - 'bright', - 'cool', - 'eye', - 'eyewear', - 'face', - 'glasses', - 'smile', - 'sun', - 'sunglasses', - 'uc6', - 'smiley', - 'happy', - 'silly', - 'glasses', - 'emojione', - 'awesome', - 'beautiful', - 'boys night', - 'smile', - 'sunglasses', - 'hawaii', - 'california', - 'florida', - 'las vegas', - 'fun', - 'summer', - 'clever', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'funny', - 'eyeglasses', - 'eye glasses', - 'emoji one', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'guys night', - 'smiles', - 'shades', - 'lunettes de soleil', - 'sun glasses', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'vegas', - 'weekend', - 'witty', - 'B-)', - 'B)', - '8)', - '8-)', - 'B-D', - '8-D' - ]), - Emoji( - name: 'star-struck', - char: '\u{1F929}', - shortName: 'star_struck', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceAffection, - keywords: [ - 'uc10', - 'smiley', - 'happy', - 'selfie', - 'fame', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'famous', - 'celebrity' - ]), - Emoji( - name: 'partying face', - char: '\u{1F973}', - shortName: 'partying_face', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceHat, - keywords: [ - 'uc11', - 'smiley', - 'holidays', - 'happy', - 'silly', - 'hat', - 'cheers', - 'happy birthday', - 'confetti', - 'celebrate', - 'fun', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'holiday', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'funny', - 'hats', - 'cap', - 'caps', - 'gān bēi', - 'Na zdravi', - 'Proost', - 'Prost', - 'Sláinte', - 'Cin cin', - 'Kanpai', - 'Na zdrowie', - 'Saúde', - 'На здоровье', - 'Salud', - 'Skål', - 'Sei gesund', - 'santé', - 'Bon anniversaire', - 'joyeux anniversaire', - 'buon compleanno', - 'feliz cumpleaños', - 'alles Gute zum Geburtstag', - 'feliz Aniversário', - 'Gratulerer med dagen', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar' - ]), - Emoji( - name: 'smirking face', - char: '\u{1F60F}', - shortName: 'smirk', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceNeutralSkeptical, - keywords: [ - 'face', - 'smirk', - 'uc6', - 'smiley', - 'happy', - 'silly', - 'sexy', - 'sarcastic', - 'smile', - 'pleased', - 'clever', - 'proud', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'funny', - 'sarcasm', - 'smiles', - 'please', - 'chill', - 'confident', - 'content', - 'witty' - ]), - Emoji( - name: 'unamused face', - char: '\u{1F612}', - shortName: 'unamused', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceNeutralSkeptical, - keywords: [ - 'face', - 'unamused', - 'unhappy', - 'uc6', - 'smiley', - 'sad', - 'tired', - 'angry', - 'bored', - 'hate', - 'doubt', - 'grimace', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'triste', - 'depression', - 'negative', - 'sadness', - 'sleepy', - 'sleep', - 'dormi', - 'pillow', - 'blanket', - 'exhausted', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'boring', - 'agree', - 'whatever', - 'boredom', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical' - ]), - Emoji( - name: 'disappointed face', - char: '\u{1F61E}', - shortName: 'disappointed', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceConcerned, - keywords: [ - 'disappointed', - 'face', - 'uc6', - 'smiley', - 'sad', - 'tired', - 'angry', - 'bored', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'triste', - 'depression', - 'negative', - 'sadness', - 'sleepy', - 'sleep', - 'dormi', - 'pillow', - 'blanket', - 'exhausted', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'boring', - 'agree', - 'whatever', - 'boredom', - '>:[', - ':-(', - ':(', - ':-[', - ':[', - '=(' - ]), - Emoji( - name: 'pensive face', - char: '\u{1F614}', - shortName: 'pensive', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceSleepy, - keywords: [ - 'dejected', - 'face', - 'pensive', - 'uc6', - 'smiley', - 'sad', - 'rip', - 'guilty', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'triste', - 'depression', - 'negative', - 'sadness', - 'rest in peace' - ]), - Emoji( - name: 'worried face', - char: '\u{1F61F}', - shortName: 'worried', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceConcerned, - keywords: [ - 'face', - 'worried', - 'uc6', - 'smiley', - 'sad', - 'angry', - 'doubt', - 'guilty', - 'jealous', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'triste', - 'depression', - 'negative', - 'sadness', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical' - ]), - Emoji( - name: 'confused face', - char: '\u{1F615}', - shortName: 'confused', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceConcerned, - keywords: [ - 'confused', - 'face', - 'uc6', - 'smiley', - 'nurse', - 'doubt', - 'what', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical', - '>:\\', - '>:/', - ':-/', - ':-.', - ':/', - ':\\', - '=/', - '=\\', - ':L', - '=L' - ]), - Emoji( - name: 'slightly frowning face', - char: '\u{1F641}', - shortName: 'slight_frown', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceConcerned, - keywords: [ - 'face', - 'frown', - 'uc7', - 'smiley', - 'sad', - 'angry', - 'hate', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'triste', - 'depression', - 'negative', - 'sadness', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no' - ]), - Emoji( - name: 'frowning face', - char: '\u{2639}\u{FE0F}', - shortName: 'frowning2', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceConcerned, - keywords: [ - 'face', - 'frown', - 'uc1', - 'smiley', - 'sad', - 'angry', - 'hate', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'triste', - 'depression', - 'negative', - 'sadness', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no' - ]), - Emoji( - name: 'persevering face', - char: '\u{1F623}', - shortName: 'persevere', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceConcerned, - keywords: [ - 'face', - 'persevere', - 'uc6', - 'smiley', - 'angry', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - '>.<' - ]), - Emoji( - name: 'confounded face', - char: '\u{1F616}', - shortName: 'confounded', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceConcerned, - keywords: [ - 'confounded', - 'face', - 'uc6', - 'smiley', - 'angry', - 'wow', - 'hate', - 'stinky', - 'ugly', - 'confused', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'surprised', - 'scared', - 'shocked', - 'whoa', - 'surprise', - 'scary', - 'nervous', - 'shaking', - 'afraid', - 'amaze', - 'amazing', - 'creepy', - 'cringe', - 'gasp', - 'anxious', - 'mind blown', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'smell', - 'stink', - 'odor', - 'perplexed' - ]), - Emoji( - name: 'tired face', - char: '\u{1F62B}', - shortName: 'tired_face', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceConcerned, - keywords: [ - 'face', - 'tired', - 'uc6', - 'smiley', - 'sad', - 'tired', - 'angry', - 'sick', - 'wow', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'triste', - 'depression', - 'negative', - 'sadness', - 'sleepy', - 'sleep', - 'dormi', - 'pillow', - 'blanket', - 'exhausted', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'surprised', - 'scared', - 'shocked', - 'whoa', - 'surprise', - 'scary', - 'nervous', - 'shaking', - 'afraid', - 'amaze', - 'amazing', - 'creepy', - 'cringe', - 'gasp', - 'anxious', - 'mind blown' - ]), - Emoji( - name: 'weary face', - char: '\u{1F629}', - shortName: 'weary', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceConcerned, - keywords: [ - 'face', - 'tired', - 'weary', - 'uc6', - 'smiley', - 'sad', - 'tired', - 'angry', - 'stressed', - 'wow', - 'shame', - 'lazy', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'triste', - 'depression', - 'negative', - 'sadness', - 'sleepy', - 'sleep', - 'dormi', - 'pillow', - 'blanket', - 'exhausted', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'surprised', - 'scared', - 'shocked', - 'whoa', - 'surprise', - 'scary', - 'nervous', - 'shaking', - 'afraid', - 'amaze', - 'amazing', - 'creepy', - 'cringe', - 'gasp', - 'anxious', - 'mind blown' - ]), - Emoji( - name: 'pleading face', - char: '\u{1F97A}', - shortName: 'pleading_face', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceConcerned, - keywords: [ - 'uc11', - 'smiley', - 'sad', - 'cry', - 'condolence', - 'omg', - 'heartbreak', - 'blush', - 'begging', - 'doubt', - 'guilty', - 'help', - 'shame', - 'hope', - 'liar', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'triste', - 'depression', - 'negative', - 'sadness', - 'crying', - 'weeping', - 'weep', - 'sob', - 'sobbing', - 'tear', - 'tears', - 'bawling', - 'compassion', - 'omfg', - 'oh my god', - 'broken heart', - 'heartbroken', - 'blushing', - 'bella', - 'embarrassed', - 'creep', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical', - 'swear', - 'promise', - 'lies', - 'lying' - ]), - Emoji( - name: 'crying face', - char: '\u{1F622}', - shortName: 'cry', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceConcerned, - keywords: [ - 'cry', - 'face', - 'sad', - 'tear', - 'uc6', - 'smiley', - 'sad', - 'cry', - 'rip', - 'heartbreak', - 'drip', - 'guilty', - 'covid', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'triste', - 'depression', - 'negative', - 'sadness', - 'crying', - 'weeping', - 'weep', - 'sob', - 'sobbing', - 'tear', - 'tears', - 'bawling', - 'rest in peace', - 'broken heart', - 'heartbroken', - ":'(", - ":'-(", - ';(', - ';-(' - ]), - Emoji( - name: 'loudly crying face', - char: '\u{1F62D}', - shortName: 'sob', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceConcerned, - keywords: [ - 'cry', - 'face', - 'sad', - 'sob', - 'tear', - 'uc6', - 'smiley', - 'sad', - 'cry', - 'rip', - 'heartbreak', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'triste', - 'depression', - 'negative', - 'sadness', - 'crying', - 'weeping', - 'weep', - 'sob', - 'sobbing', - 'tear', - 'tears', - 'bawling', - 'rest in peace', - 'broken heart', - 'heartbroken' - ]), - Emoji( - name: 'face with steam from nose', - char: '\u{1F624}', - shortName: 'triumph', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceNegative, - keywords: [ - 'face', - 'triumph', - 'won', - 'uc6', - 'smiley', - 'angry', - 'smoking', - 'steam', - 'breathe', - 'proud', - 'festivus', - 'booger', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'smoke', - 'cigarette', - 'puff', - 'steaming', - 'piping', - 'sigh', - 'inhale' - ]), - Emoji( - name: 'angry face', - char: '\u{1F620}', - shortName: 'angry', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceNegative, - keywords: [ - 'angry', - 'face', - 'mad', - 'uc6', - 'smiley', - 'angry', - 'hate', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - '>:(', - '>:-(', - ':@' - ]), - Emoji( - name: 'pouting face', - char: '\u{1F621}', - shortName: 'rage', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceNegative, - keywords: [ - 'angry', - 'face', - 'mad', - 'pouting', - 'rage', - 'red', - 'uc6', - 'smiley', - 'angry', - 'hate', - 'bitch', - 'donald trump', - 'guilty', - 'las vegas', - 'killer', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'puta', - 'pute', - 'trump', - 'vegas', - 'savage', - 'scary clown' - ]), - Emoji( - name: 'face with symbols on mouth', - char: '\u{1F92C}', - shortName: 'face_with_symbols_over_mouth', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceNegative, - keywords: [ - 'uc10', - 'smiley', - 'angry', - 'hate', - 'donald trump', - 'swearing', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'trump', - 'cussing', - 'cursing' - ]), - Emoji( - name: 'exploding head', - char: '\u{1F92F}', - shortName: 'exploding_head', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceUnwell, - keywords: [ - 'shocked', - 'uc10', - 'smiley', - 'angry', - 'wow', - 'omg', - 'donald trump', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'surprised', - 'scared', - 'shocked', - 'whoa', - 'surprise', - 'scary', - 'nervous', - 'shaking', - 'afraid', - 'amaze', - 'amazing', - 'creepy', - 'cringe', - 'gasp', - 'anxious', - 'mind blown', - 'omfg', - 'oh my god', - 'trump' - ]), - Emoji( - name: 'flushed face', - char: '\u{1F633}', - shortName: 'flushed', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceConcerned, - keywords: [ - 'dazed', - 'face', - 'flushed', - 'uc6', - 'smiley', - 'omg', - 'blush', - 'guilty', - 'porn', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'omfg', - 'oh my god', - 'blushing', - 'bella', - 'embarrassed', - 'creep', - ':\$', - '=\$' - ]), - Emoji( - name: 'hot face', - char: '\u{1F975}', - shortName: 'hot_face', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceUnwell, - keywords: [ - 'uc11', - 'weather', - 'smiley', - 'stressed', - 'sweat', - 'hot', - 'hate', - 'summer', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'heat', - 'warm', - 'caliente', - 'chaud', - 'heiß', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'weekend' - ]), - Emoji( - name: 'cold face', - char: '\u{1F976}', - shortName: 'cold_face', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceUnwell, - keywords: [ - 'uc11', - 'weather', - 'smiley', - 'winter', - 'snow', - 'cold', - 'grimace', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'freeze', - 'frozen', - 'frost', - 'ice cube', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles' - ]), - Emoji( - name: 'face screaming in fear', - char: '\u{1F631}', - shortName: 'scream', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceConcerned, - keywords: [ - 'face', - 'fear', - 'fearful', - 'munch', - 'scared', - 'scream', - 'uc6', - 'smiley', - 'halloween', - 'wow', - 'omg', - 'donald trump', - 'fame', - 'porn', - 'ugly', - 'what', - 'crazy', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'samhain', - 'surprised', - 'scared', - 'shocked', - 'whoa', - 'surprise', - 'scary', - 'nervous', - 'shaking', - 'afraid', - 'amaze', - 'amazing', - 'creepy', - 'cringe', - 'gasp', - 'anxious', - 'mind blown', - 'omfg', - 'oh my god', - 'trump', - 'famous', - 'celebrity', - 'weird', - 'awkward', - 'insane', - 'wild' - ]), - Emoji( - name: 'fearful face', - char: '\u{1F628}', - shortName: 'fearful', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceConcerned, - keywords: [ - 'face', - 'fear', - 'fearful', - 'scared', - 'uc6', - 'smiley', - 'halloween', - 'stressed', - 'wow', - 'guilty', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'samhain', - 'surprised', - 'scared', - 'shocked', - 'whoa', - 'surprise', - 'scary', - 'nervous', - 'shaking', - 'afraid', - 'amaze', - 'amazing', - 'creepy', - 'cringe', - 'gasp', - 'anxious', - 'mind blown', - 'D:' - ]), - Emoji( - name: 'anxious face with sweat', - char: '\u{1F630}', - shortName: 'cold_sweat', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceConcerned, - keywords: [ - 'blue', - 'cold', - 'face', - 'mouth', - 'open', - 'rushed', - 'sweat', - 'uc6', - 'smiley', - 'halloween', - 'angry', - 'stressed', - 'sweat', - 'drip', - 'porn', - 'covid', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'samhain', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad' - ]), - Emoji( - name: 'sad but relieved face', - char: '\u{1F625}', - shortName: 'disappointed_relieved', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceConcerned, - keywords: [ - 'disappointed', - 'face', - 'relieved', - 'whew', - 'uc6', - 'smiley', - 'sad', - 'cry', - 'stressed', - 'sweat', - 'calm', - 'drip', - 'guilty', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'triste', - 'depression', - 'negative', - 'sadness', - 'crying', - 'weeping', - 'weep', - 'sob', - 'sobbing', - 'tear', - 'tears', - 'bawling' - ]), - Emoji( - name: 'downcast face with sweat', - char: '\u{1F613}', - shortName: 'sweat', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceConcerned, - keywords: [ - 'cold', - 'face', - 'sweat', - 'uc6', - 'smiley', - 'sad', - 'stressed', - 'sweat', - 'drip', - 'guilty', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'triste', - 'depression', - 'negative', - 'sadness', - "':(", - "':-(", - "'=(" - ]), - Emoji( - name: 'hugging face', - char: '\u{1F917}', - shortName: 'hugging', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceHand, - keywords: [ - 'face', - 'hug', - 'hugging', - 'uc8', - 'smiley', - 'happy', - 'tired', - 'love', - 'hug', - 'thank you', - 'friend', - 'blush', - 'facebook', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'sleepy', - 'sleep', - 'dormi', - 'pillow', - 'blanket', - 'exhausted', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'embrace', - 'hugs', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'blushing', - 'bella', - 'embarrassed', - 'creep' - ]), - Emoji( - name: 'thinking face', - char: '\u{1F914}', - shortName: 'thinking', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceHand, - keywords: [ - 'face', - 'thinking', - 'uc8', - 'smiley', - 'boys night', - 'dream', - 'brain', - 'doubt', - 'idea', - 'confused', - 'what', - 'mystery', - 'innovate', - 'question', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'guys night', - 'dreams', - 'mind', - 'memory', - 'thought', - 'conscience', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical', - 'perplexed', - 'innovation', - 'inquire', - 'quiz', - 'puzzled' - ]), - Emoji( - name: 'face with hand over mouth', - char: '\u{1F92D}', - shortName: 'face_with_hand_over_mouth', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceHand, - keywords: [ - 'uc10', - 'smiley', - 'tired', - 'blush', - 'tease', - 'quiet', - 'what', - 'secret', - 'yawn', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'sleepy', - 'sleep', - 'dormi', - 'pillow', - 'blanket', - 'exhausted', - 'blushing', - 'bella', - 'embarrassed', - 'creep', - 'joke', - 'kidding', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'shhhhh' - ]), - Emoji( - name: 'yawning face', - char: '\u{1F971}', - shortName: 'yawning_face', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceConcerned, - keywords: [ - 'uc12', - 'smiley', - 'tired', - 'goodnight', - 'bored', - 'calm', - 'quiet', - 'wait', - 'yawn', - 'lazy', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'sleepy', - 'sleep', - 'dormi', - 'pillow', - 'blanket', - 'exhausted', - 'boring', - 'agree', - 'whatever', - 'boredom', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'hours' - ]), - Emoji( - name: 'shushing face', - char: '\u{1F92B}', - shortName: 'shushing_face', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceHand, - keywords: [ - 'quiet', - 'shush', - 'uc10', - 'smiley', - 'quiet', - 'secret', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'shhhhh' - ]), - Emoji( - name: 'lying face', - char: '\u{1F925}', - shortName: 'lying_face', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceNeutralSkeptical, - keywords: [ - 'face', - 'lie', - 'pinocchio', - 'uc9', - 'smiley', - 'donald trump', - 'guilty', - 'crazy', - 'liar', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'trump', - 'weird', - 'awkward', - 'insane', - 'wild', - 'lies', - 'lying' - ]), - Emoji( - name: 'face without mouth', - char: '\u{1F636}', - shortName: 'no_mouth', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceNeutralSkeptical, - keywords: [ - 'face', - 'mouth', - 'quiet', - 'silent', - 'uc6', - 'smiley', - 'neutral', - 'hate', - 'dumb', - 'quiet', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'idiot', - 'ignorant', - 'stupid', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - ':-X', - ':X', - ':-#', - ':#', - '=X', - '=#' - ]), - Emoji( - name: 'neutral face', - char: '\u{1F610}', - shortName: 'neutral_face', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceNeutralSkeptical, - keywords: [ - 'deadpan', - 'face', - 'neutral', - 'uc6', - 'smiley', - 'shrug', - 'neutral', - 'bored', - 'calm', - 'doubt', - 'dumb', - 'quiet', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'boring', - 'agree', - 'whatever', - 'boredom', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical', - 'idiot', - 'ignorant', - 'stupid', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh' - ]), - Emoji( - name: 'expressionless face', - char: '\u{1F611}', - shortName: 'expressionless', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceNeutralSkeptical, - keywords: [ - 'expressionless', - 'face', - 'inexpressive', - 'unexpressive', - 'uc6', - 'smiley', - 'neutral', - 'bored', - 'calm', - 'doubt', - 'dumb', - 'quiet', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'boring', - 'agree', - 'whatever', - 'boredom', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical', - 'idiot', - 'ignorant', - 'stupid', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - '-_-', - '-__-', - '-___-' - ]), - Emoji( - name: 'grimacing face', - char: '\u{1F62C}', - shortName: 'grimacing', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceNeutralSkeptical, - keywords: [ - 'face', - 'grimace', - 'uc6', - 'smiley', - 'silly', - 'selfie', - 'teeth', - 'grimace', - 'help', - 'porn', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'funny', - 'dentist' - ]), - Emoji( - name: 'face with rolling eyes', - char: '\u{1F644}', - shortName: 'rolling_eyes', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceNeutralSkeptical, - keywords: [ - 'eyes', - 'face', - 'rolling', - 'uc8', - 'smiley', - 'rolling eyes', - 'sarcastic', - 'bored', - 'hate', - 'doubt', - 'eyeroll', - 'jealous', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'eye roll', - 'side eye', - 'sarcasm', - 'boring', - 'agree', - 'whatever', - 'boredom', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical' - ]), - Emoji( - name: 'hushed face', - char: '\u{1F62F}', - shortName: 'hushed', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceConcerned, - keywords: [ - 'face', - 'hushed', - 'stunned', - 'surprised', - 'uc6', - 'smiley', - 'wow', - 'what', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'surprised', - 'scared', - 'shocked', - 'whoa', - 'surprise', - 'scary', - 'nervous', - 'shaking', - 'afraid', - 'amaze', - 'amazing', - 'creepy', - 'cringe', - 'gasp', - 'anxious', - 'mind blown' - ]), - Emoji( - name: 'frowning face with open mouth', - char: '\u{1F626}', - shortName: 'frowning', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceConcerned, - keywords: [ - 'face', - 'frown', - 'mouth', - 'open', - 'uc6', - 'smiley', - 'sad', - 'jealous', - 'what', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'triste', - 'depression', - 'negative', - 'sadness' - ]), - Emoji( - name: 'anguished face', - char: '\u{1F627}', - shortName: 'anguished', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceConcerned, - keywords: [ - 'anguished', - 'face', - 'uc6', - 'smiley', - 'sad', - 'stressed', - 'wow', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'triste', - 'depression', - 'negative', - 'sadness', - 'surprised', - 'scared', - 'shocked', - 'whoa', - 'surprise', - 'scary', - 'nervous', - 'shaking', - 'afraid', - 'amaze', - 'amazing', - 'creepy', - 'cringe', - 'gasp', - 'anxious', - 'mind blown' - ]), - Emoji( - name: 'face with open mouth', - char: '\u{1F62E}', - shortName: 'open_mouth', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceConcerned, - keywords: [ - 'face', - 'mouth', - 'open', - 'sympathy', - 'uc6', - 'smiley', - 'wow', - 'dumb', - 'what', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'surprised', - 'scared', - 'shocked', - 'whoa', - 'surprise', - 'scary', - 'nervous', - 'shaking', - 'afraid', - 'amaze', - 'amazing', - 'creepy', - 'cringe', - 'gasp', - 'anxious', - 'mind blown', - 'idiot', - 'ignorant', - 'stupid', - ':-O', - ':O', - 'O_O', - '>:O' - ]), - Emoji( - name: 'astonished face', - char: '\u{1F632}', - shortName: 'astonished', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceConcerned, - keywords: [ - 'astonished', - 'face', - 'shocked', - 'totally', - 'uc6', - 'smiley', - 'wow', - 'omg', - 'donald trump', - 'crazy', - 'mystery', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'surprised', - 'scared', - 'shocked', - 'whoa', - 'surprise', - 'scary', - 'nervous', - 'shaking', - 'afraid', - 'amaze', - 'amazing', - 'creepy', - 'cringe', - 'gasp', - 'anxious', - 'mind blown', - 'omfg', - 'oh my god', - 'trump', - 'weird', - 'awkward', - 'insane', - 'wild' - ]), - Emoji( - name: 'sleeping face', - char: '\u{1F634}', - shortName: 'sleeping', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceSleepy, - keywords: [ - 'face', - 'sleep', - 'zzz', - 'uc6', - 'smiley', - 'tired', - 'goodnight', - 'coffee', - 'dream', - 'calm', - 'lazy', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'sleepy', - 'sleep', - 'dormi', - 'pillow', - 'blanket', - 'exhausted', - 'starbucks', - 'dreams' - ]), - Emoji( - name: 'drooling face', - char: '\u{1F924}', - shortName: 'drooling_face', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceSleepy, - keywords: [ - 'drooling', - 'face', - 'uc9', - 'smiley', - 'beautiful', - 'dumb', - 'porn', - 'ugly', - 'what', - 'crazy', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'idiot', - 'ignorant', - 'stupid', - 'weird', - 'awkward', - 'insane', - 'wild' - ]), - Emoji( - name: 'sleepy face', - char: '\u{1F62A}', - shortName: 'sleepy', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceSleepy, - keywords: [ - 'face', - 'sleep', - 'uc6', - 'smiley', - 'sad', - 'sick', - 'costume', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'triste', - 'depression', - 'negative', - 'sadness', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew' - ]), - Emoji( - name: 'dizzy face', - char: '\u{1F635}', - shortName: 'dizzy_face', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceUnwell, - keywords: [ - 'dizzy', - 'face', - 'uc6', - 'smiley', - 'dead', - 'wow', - 'nutcase', - 'omg', - 'hate', - 'drunk', - 'dumb', - 'las vegas', - 'what', - 'crazy', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'death', - 'die', - 'dying', - 'fart', - 'goth', - 'grave', - 'headstone', - 'horror', - 'hurt', - 'kill', - 'murder', - 'tomb', - 'toot', - 'died', - 'surprised', - 'scared', - 'shocked', - 'whoa', - 'surprise', - 'scary', - 'nervous', - 'shaking', - 'afraid', - 'amaze', - 'amazing', - 'creepy', - 'cringe', - 'gasp', - 'anxious', - 'mind blown', - 'omfg', - 'oh my god', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'flustered', - 'dizzy', - 'idiot', - 'ignorant', - 'stupid', - 'vegas', - 'weird', - 'awkward', - 'insane', - 'wild', - '#-)', - '#)', - '%-)', - '%)', - 'X)', - 'X-)' - ]), - Emoji( - name: 'zipper-mouth face', - char: '\u{1F910}', - shortName: 'zipper_mouth', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceNeutralSkeptical, - keywords: [ - 'face', - 'mouth', - 'zipper', - 'uc8', - 'smiley', - 'angry', - 'fight', - 'dumb', - 'quiet', - 'crazy', - 'secret', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'idiot', - 'ignorant', - 'stupid', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'weird', - 'awkward', - 'insane', - 'wild', - 'shhhhh' - ]), - Emoji( - name: 'woozy face', - char: '\u{1F974}', - shortName: 'woozy_face', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceUnwell, - keywords: [ - 'uc11', - 'smiley', - 'silly', - 'drugs', - 'sick', - 'drunk', - 'dumb', - 'ugly', - 'crazy', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'funny', - 'drug', - 'narcotics', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'flustered', - 'dizzy', - 'idiot', - 'ignorant', - 'stupid', - 'weird', - 'awkward', - 'insane', - 'wild' - ]), - Emoji( - name: 'nauseated face', - char: '\u{1F922}', - shortName: 'nauseated_face', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceUnwell, - keywords: [ - 'face', - 'nauseated', - 'vomit', - 'uc9', - 'smiley', - 'bathroom', - 'sick', - 'hate', - 'drunk', - 'stinky', - 'donald trump', - 'poison', - 'full', - 'Nauseated', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'flustered', - 'dizzy', - 'smell', - 'stink', - 'odor', - 'trump', - 'toxic', - 'toxins', - 'green face' - ]), - Emoji( - name: 'face vomiting', - char: '\u{1F92E}', - shortName: 'face_vomiting', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceUnwell, - keywords: [ - 'sick', - 'vomit', - 'uc10', - 'smiley', - 'bathroom', - 'sick', - 'hate', - 'donald trump', - 'Nauseated', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'trump', - 'green face' - ]), - Emoji( - name: 'sneezing face', - char: '\u{1F927}', - shortName: 'sneezing_face', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceUnwell, - keywords: [ - 'face', - 'gesundheit', - 'sneeze', - 'uc9', - 'smiley', - 'sick', - 'nurse', - 'stinky', - 'booger', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'smell', - 'stink', - 'odor' - ]), - Emoji( - name: 'face with medical mask', - char: '\u{1F637}', - shortName: 'mask', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceUnwell, - keywords: [ - 'cold', - 'doctor', - 'face', - 'mask', - 'medicine', - 'sick', - 'uc6', - 'smiley', - 'dead', - 'health', - 'sick', - 'teeth', - 'nurse', - 'clean', - 'poison', - 'mask', - 'virus', - 'covid', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'death', - 'die', - 'dying', - 'fart', - 'goth', - 'grave', - 'headstone', - 'horror', - 'hurt', - 'kill', - 'murder', - 'tomb', - 'toot', - 'died', - 'medicine', - 'doctor', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'dentist', - 'toxic', - 'toxins', - 'corona' - ]), - Emoji( - name: 'face with thermometer', - char: '\u{1F912}', - shortName: 'thermometer_face', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceUnwell, - keywords: [ - 'face', - 'ill', - 'sick', - 'thermometer', - 'uc8', - 'smiley', - 'health', - 'sick', - 'nurse', - 'virus', - 'covid', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'medicine', - 'doctor', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'corona' - ]), - Emoji( - name: 'face with head-bandage', - char: '\u{1F915}', - shortName: 'head_bandage', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceUnwell, - keywords: [ - 'bandage', - 'face', - 'hurt', - 'injury', - 'uc8', - 'smiley', - 'health', - 'sick', - 'nurse', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'medicine', - 'doctor', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew' - ]), - Emoji( - name: 'money-mouth face', - char: '\u{1F911}', - shortName: 'money_mouth', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceTongue, - keywords: [ - 'face', - 'money', - 'mouth', - 'uc8', - 'smiley', - 'money', - 'win', - 'boys night', - 'power', - 'stinky', - 'coins', - 'discount', - 'donald trump', - 'jealous', - 'las vegas', - 'rich', - 'greed', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'cash', - 'dollars', - 'dollar', - 'bucks', - 'currency', - 'funds', - 'payment', - 'money face', - 'reward', - 'thief', - 'bank', - 'benjamins', - 'argent', - 'dinero', - 'i soldi', - 'Geld', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'guys night', - 'smell', - 'stink', - 'odor', - 'sale', - 'bargain', - 'trump', - 'vegas', - 'grand', - 'expensive', - 'fancy', - 'selfish' - ]), - Emoji( - name: 'cowboy hat face', - char: '\u{1F920}', - shortName: 'cowboy', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceHat, - keywords: [ - 'cowboy', - 'cowgirl', - 'face', - 'hat', - 'uc9', - 'smiley', - 'america', - 'hat', - 'halloween', - 'boys night', - 'magic', - 'disney', - 'fame', - 'super hero', - 'texas', - 'costume', - 'independence day', - 'disguise', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'usa', - 'united states', - 'united states of america', - 'american', - 'hats', - 'cap', - 'caps', - 'samhain', - 'guys night', - 'spell', - 'genie', - 'magical', - 'cartoon', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman', - '4th of july' - ]), - Emoji( - name: 'disguised face', - char: '\u{1F978}', - shortName: 'disguised_face', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceHat, - keywords: [ - 'uc13', - 'smiley', - 'silly', - 'halloween', - 'eyes', - 'boys night', - 'celebrate', - 'crazy', - 'mystery', - 'costume', - 'clever', - 'disguise', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'funny', - 'samhain', - 'eye', - 'eyebrow', - 'guys night', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'weird', - 'awkward', - 'insane', - 'wild', - 'witty' - ]), - Emoji( - name: 'smiling face with horns', - char: '\u{1F608}', - shortName: 'smiling_imp', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceNegative, - keywords: [ - 'face', - 'fairy tale', - 'fantasy', - 'horns', - 'smile', - 'uc6', - 'smiley', - 'silly', - 'halloween', - 'angry', - 'monster', - 'boys night', - 'evil', - 'guilty', - 'jealous', - 'porn', - 'crazy', - 'killer', - 'disguise', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'funny', - 'samhain', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'monsters', - 'beast', - 'guys night', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'weird', - 'awkward', - 'insane', - 'wild', - 'savage', - 'scary clown' - ]), - Emoji( - name: 'angry face with horns', - char: '\u{1F47F}', - shortName: 'imp', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceNegative, - keywords: [ - 'demon', - 'devil', - 'face', - 'fairy tale', - 'fantasy', - 'imp', - 'uc6', - 'smiley', - 'halloween', - 'angry', - 'monster', - 'wth', - 'fight', - 'evil', - 'dumb', - 'vampire', - 'crazy', - 'killer', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'samhain', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'monsters', - 'beast', - 'what the hell', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'idiot', - 'ignorant', - 'stupid', - 'dracula', - 'weird', - 'awkward', - 'insane', - 'wild', - 'savage', - 'scary clown' - ]), - Emoji( - name: 'ogre', - char: '\u{1F479}', - shortName: 'japanese_ogre', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceCostume, - keywords: [ - 'uc6', - 'halloween', - 'japan', - 'angry', - 'monster', - 'wow', - 'evil', - 'super hero', - 'ugly', - 'crazy', - 'killer', - 'disguise', - 'samhain', - 'japanese', - 'ninja', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'monsters', - 'beast', - 'surprised', - 'scared', - 'shocked', - 'whoa', - 'surprise', - 'scary', - 'nervous', - 'shaking', - 'afraid', - 'amaze', - 'amazing', - 'creepy', - 'cringe', - 'gasp', - 'anxious', - 'mind blown', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'superhero', - 'superman', - 'batman', - 'weird', - 'awkward', - 'insane', - 'wild', - 'savage', - 'scary clown' - ]), - Emoji( - name: 'goblin', - char: '\u{1F47A}', - shortName: 'japanese_goblin', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceCostume, - keywords: [ - 'creature', - 'face', - 'fairy tale', - 'fantasy', - 'monster', - 'uc6', - 'halloween', - 'japan', - 'angry', - 'monster', - 'wow', - 'mustache', - 'evil', - 'super hero', - 'ugly', - 'crazy', - 'killer', - 'mask', - 'disguise', - 'samhain', - 'japanese', - 'ninja', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'monsters', - 'beast', - 'surprised', - 'scared', - 'shocked', - 'whoa', - 'surprise', - 'scary', - 'nervous', - 'shaking', - 'afraid', - 'amaze', - 'amazing', - 'creepy', - 'cringe', - 'gasp', - 'anxious', - 'mind blown', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'superhero', - 'superman', - 'batman', - 'weird', - 'awkward', - 'insane', - 'wild', - 'savage', - 'scary clown' - ]), - Emoji( - name: 'clown face', - char: '\u{1F921}', - shortName: 'clown', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceCostume, - keywords: [ - 'clown', - 'face', - 'uc9', - 'smiley', - 'silly', - 'halloween', - 'laugh', - 'circus', - 'magic', - 'donald trump', - 'mcdonalds', - 'super hero', - 'crazy', - 'killer', - 'costume', - 'disguise', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'funny', - 'samhain', - 'laughing', - 'lol', - 'rofl', - 'lmao', - 'lmfao', - 'hilarious', - 'ha', - 'haha', - 'chuckle', - 'comedy', - 'giggle', - 'hehe', - 'joyful', - 'laugh out loud', - 'rire', - 'tee hee', - 'jaja', - 'circus tent', - 'clown', - 'clowns', - 'spell', - 'genie', - 'magical', - 'trump', - 'ronald mcdonald', - 'macdo', - 'superhero', - 'superman', - 'batman', - 'weird', - 'awkward', - 'insane', - 'wild', - 'savage', - 'scary clown' - ]), - Emoji( - name: 'pile of poo', - char: '\u{1F4A9}', - shortName: 'poop', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceCostume, - keywords: [ - 'comic', - 'dung', - 'face', - 'monster', - 'poo', - 'poop', - 'uc6', - 'silly', - 'bathroom', - 'dead', - 'sol', - 'diarrhea', - 'shit', - 'bitch', - 'stinky', - 'donald trump', - 'dumb', - 'ugly', - 'funny', - 'death', - 'die', - 'dying', - 'fart', - 'goth', - 'grave', - 'headstone', - 'horror', - 'hurt', - 'kill', - 'murder', - 'tomb', - 'toot', - 'died', - 'shit outta luck', - 'shit out of luck', - 'bad luck', - 'shits', - 'the shits', - 'poop', - 'turd', - 'feces', - 'pile', - 'merde', - 'butthole', - 'caca', - 'crap', - 'dirty', - 'pooo', - 'mess', - 'brown', - 'poopoo', - 'puta', - 'pute', - 'smell', - 'stink', - 'odor', - 'trump', - 'idiot', - 'ignorant', - 'stupid' - ]), - Emoji( - name: 'ghost', - char: '\u{1F47B}', - shortName: 'ghost', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceCostume, - keywords: [ - 'creature', - 'face', - 'fairy tale', - 'fantasy', - 'monster', - 'uc6', - 'holidays', - 'halloween', - 'dead', - 'monster', - 'wow', - 'disney', - 'pacman', - 'disguise', - 'holiday', - 'samhain', - 'death', - 'die', - 'dying', - 'fart', - 'goth', - 'grave', - 'headstone', - 'horror', - 'hurt', - 'kill', - 'murder', - 'tomb', - 'toot', - 'died', - 'monsters', - 'beast', - 'surprised', - 'scared', - 'shocked', - 'whoa', - 'surprise', - 'scary', - 'nervous', - 'shaking', - 'afraid', - 'amaze', - 'amazing', - 'creepy', - 'cringe', - 'gasp', - 'anxious', - 'mind blown', - 'cartoon', - 'pac man' - ]), - Emoji( - name: 'skull', - char: '\u{1F480}', - shortName: 'skull', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceNegative, - keywords: [ - 'death', - 'face', - 'fairy tale', - 'monster', - 'uc6', - 'halloween', - 'dead', - 'skull', - 'wow', - 'harry potter', - 'pirate', - 'poison', - 'super hero', - 'killer', - 'bones', - 'samhain', - 'death', - 'die', - 'dying', - 'fart', - 'goth', - 'grave', - 'headstone', - 'horror', - 'hurt', - 'kill', - 'murder', - 'tomb', - 'toot', - 'died', - 'skull and crossbones', - 'skeleton', - 'surprised', - 'scared', - 'shocked', - 'whoa', - 'surprise', - 'scary', - 'nervous', - 'shaking', - 'afraid', - 'amaze', - 'amazing', - 'creepy', - 'cringe', - 'gasp', - 'anxious', - 'mind blown', - 'toxic', - 'toxins', - 'superhero', - 'superman', - 'batman', - 'savage', - 'scary clown', - 'Os', - 'hueso' - ]), - Emoji( - name: 'skull and crossbones', - char: '\u{2620}\u{FE0F}', - shortName: 'skull_crossbones', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceNegative, - keywords: [ - 'crossbones', - 'death', - 'face', - 'monster', - 'skull', - 'uc1', - 'halloween', - 'dead', - 'skull', - 'wow', - 'deadpool', - 'pirate', - 'danger', - 'disney', - 'poison', - 'killer', - 'bones', - 'samhain', - 'death', - 'die', - 'dying', - 'fart', - 'goth', - 'grave', - 'headstone', - 'horror', - 'hurt', - 'kill', - 'murder', - 'tomb', - 'toot', - 'died', - 'skull and crossbones', - 'skeleton', - 'surprised', - 'scared', - 'shocked', - 'whoa', - 'surprise', - 'scary', - 'nervous', - 'shaking', - 'afraid', - 'amaze', - 'amazing', - 'creepy', - 'cringe', - 'gasp', - 'anxious', - 'mind blown', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'cartoon', - 'toxic', - 'toxins', - 'savage', - 'scary clown', - 'Os', - 'hueso' - ]), - Emoji( - name: 'alien', - char: '\u{1F47D}', - shortName: 'alien', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceCostume, - keywords: [ - 'creature', - 'extraterrestrial', - 'face', - 'fairy tale', - 'fantasy', - 'monster', - 'ufo', - 'uc6', - 'halloween', - 'space', - 'monster', - 'alien', - 'scientology', - 'star wars', - 'disguise', - 'samhain', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'monsters', - 'beast', - 'ufo', - 'scientologist' - ]), - Emoji( - name: 'alien monster', - char: '\u{1F47E}', - shortName: 'space_invader', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceCostume, - keywords: [ - 'alien', - 'creature', - 'extraterrestrial', - 'face', - 'fairy tale', - 'fantasy', - 'monster', - 'ufo', - 'uc6', - 'halloween', - 'space', - 'monster', - 'alien', - 'star wars', - 'vintage', - 'pacman', - 'samhain', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'monsters', - 'beast', - 'ufo', - 'pac man' - ]), - Emoji( - name: 'robot', - char: '\u{1F916}', - shortName: 'robot', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceCostume, - keywords: [ - 'face', - 'monster', - 'robot', - 'uc8', - 'halloween', - 'monster', - 'disney', - 'drone', - 'samhain', - 'monsters', - 'beast', - 'cartoon' - ]), - Emoji( - name: 'jack-o-lantern', - char: '\u{1F383}', - shortName: 'jack_o_lantern', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.event, - keywords: [ - 'celebration', - 'halloween', - 'jack', - 'lantern', - 'uc6', - 'holidays', - 'halloween', - 'pumpkin', - 'minecraft', - 'holiday', - 'samhain', - 'jack o lantern', - 'zucca', - 'citrouille' - ]), - Emoji( - name: 'grinning cat', - char: '\u{1F63A}', - shortName: 'smiley_cat', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.catFace, - keywords: [ - 'cat', - 'face', - 'mouth', - 'open', - 'smile', - 'uc6', - 'animal', - 'happy', - 'silly', - 'cat', - 'animals', - 'animal kingdom', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'funny', - 'kitty', - 'kitten', - 'cats', - 'kittens', - 'kitties', - 'feline', - 'felines', - 'cat face', - 'gato', - 'meow' - ]), - Emoji( - name: 'grinning cat with smiling eyes', - char: '\u{1F638}', - shortName: 'smile_cat', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.catFace, - keywords: [ - 'cat', - 'eye', - 'face', - 'grin', - 'smile', - 'uc6', - 'animal', - 'happy', - 'silly', - 'cat', - 'porn', - 'animals', - 'animal kingdom', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'funny', - 'kitty', - 'kitten', - 'cats', - 'kittens', - 'kitties', - 'feline', - 'felines', - 'cat face', - 'gato', - 'meow' - ]), - Emoji( - name: 'cat with tears of joy', - char: '\u{1F639}', - shortName: 'joy_cat', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.catFace, - keywords: [ - 'cat', - 'face', - 'joy', - 'tear', - 'uc6', - 'animal', - 'happy', - 'silly', - 'cry', - 'laugh', - 'cat', - 'sarcastic', - 'tease', - 'animals', - 'animal kingdom', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'funny', - 'crying', - 'weeping', - 'weep', - 'sob', - 'sobbing', - 'tear', - 'tears', - 'bawling', - 'laughing', - 'lol', - 'rofl', - 'lmao', - 'lmfao', - 'hilarious', - 'ha', - 'haha', - 'chuckle', - 'comedy', - 'giggle', - 'hehe', - 'joyful', - 'laugh out loud', - 'rire', - 'tee hee', - 'jaja', - 'kitty', - 'kitten', - 'cats', - 'kittens', - 'kitties', - 'feline', - 'felines', - 'cat face', - 'gato', - 'meow', - 'sarcasm', - 'joke', - 'kidding' - ]), - Emoji( - name: 'smiling cat with heart-eyes', - char: '\u{1F63B}', - shortName: 'heart_eyes_cat', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.catFace, - keywords: [ - 'cat', - 'eye', - 'face', - 'love', - 'smile', - 'uc6', - 'animal', - 'happy', - 'love', - 'cat', - 'heart eyes', - 'beautiful', - 'pussy', - 'porn', - 'animals', - 'animal kingdom', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'kitty', - 'kitten', - 'cats', - 'kittens', - 'kitties', - 'feline', - 'felines', - 'cat face', - 'gato', - 'meow', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'condom' - ]), - Emoji( - name: 'cat with wry smile', - char: '\u{1F63C}', - shortName: 'smirk_cat', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.catFace, - keywords: [ - 'cat', - 'face', - 'ironic', - 'smile', - 'wry', - 'uc6', - 'animal', - 'cat', - 'animals', - 'animal kingdom', - 'kitty', - 'kitten', - 'cats', - 'kittens', - 'kitties', - 'feline', - 'felines', - 'cat face', - 'gato', - 'meow' - ]), - Emoji( - name: 'kissing cat', - char: '\u{1F63D}', - shortName: 'kissing_cat', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.catFace, - keywords: [ - 'cat', - 'eye', - 'face', - 'kiss', - 'uc6', - 'animal', - 'love', - 'cat', - 'kisses', - 'animals', - 'animal kingdom', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'kitty', - 'kitten', - 'cats', - 'kittens', - 'kitties', - 'feline', - 'felines', - 'cat face', - 'gato', - 'meow', - 'bisous', - 'beijos', - 'besos', - 'bise', - 'blowing kisses', - 'kissy' - ]), - Emoji( - name: 'weary cat', - char: '\u{1F640}', - shortName: 'scream_cat', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.catFace, - keywords: [ - 'cat', - 'face', - 'oh', - 'surprised', - 'weary', - 'uc6', - 'animal', - 'cat', - 'animals', - 'animal kingdom', - 'kitty', - 'kitten', - 'cats', - 'kittens', - 'kitties', - 'feline', - 'felines', - 'cat face', - 'gato', - 'meow' - ]), - Emoji( - name: 'crying cat', - char: '\u{1F63F}', - shortName: 'crying_cat_face', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.catFace, - keywords: [ - 'cat', - 'cry', - 'face', - 'sad', - 'tear', - 'uc6', - 'animal', - 'cry', - 'cat', - 'animals', - 'animal kingdom', - 'crying', - 'weeping', - 'weep', - 'sob', - 'sobbing', - 'tear', - 'tears', - 'bawling', - 'kitty', - 'kitten', - 'cats', - 'kittens', - 'kitties', - 'feline', - 'felines', - 'cat face', - 'gato', - 'meow' - ]), - Emoji( - name: 'pouting cat', - char: '\u{1F63E}', - shortName: 'pouting_cat', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.catFace, - keywords: [ - 'cat', - 'face', - 'pouting', - 'uc6', - 'animal', - 'cat', - 'animals', - 'animal kingdom', - 'kitty', - 'kitten', - 'cats', - 'kittens', - 'kitties', - 'feline', - 'felines', - 'cat face', - 'gato', - 'meow' - ]), - Emoji( - name: 'palms up together', - char: '\u{1F932}', - shortName: 'palms_up_together', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'uc10', - 'diversity', - 'body', - 'hands', - 'pray', - 'soul', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering' - ]), - Emoji( - name: 'palms up together: light skin tone', - char: '\u{1F932}\u{1F3FB}', - shortName: 'palms_up_together_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'light skin tone', - 'prayer', - 'uc10', - 'diversity', - 'body', - 'hands', - 'pray', - 'soul', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering' - ], - modifiable: true), - Emoji( - name: 'palms up together: medium-light skin tone', - char: '\u{1F932}\u{1F3FC}', - shortName: 'palms_up_together_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'medium-light skin tone', - 'prayer', - 'uc10', - 'diversity', - 'body', - 'hands', - 'pray', - 'soul', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering' - ], - modifiable: true), - Emoji( - name: 'palms up together: medium skin tone', - char: '\u{1F932}\u{1F3FD}', - shortName: 'palms_up_together_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'medium skin tone', - 'prayer', - 'uc10', - 'diversity', - 'body', - 'hands', - 'pray', - 'soul', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering' - ], - modifiable: true), - Emoji( - name: 'palms up together: medium-dark skin tone', - char: '\u{1F932}\u{1F3FE}', - shortName: 'palms_up_together_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'medium-dark skin tone', - 'prayer', - 'uc10', - 'diversity', - 'body', - 'hands', - 'pray', - 'soul', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering' - ], - modifiable: true), - Emoji( - name: 'palms up together: dark skin tone', - char: '\u{1F932}\u{1F3FF}', - shortName: 'palms_up_together_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'dark skin tone', - 'prayer', - 'uc10', - 'diversity', - 'body', - 'hands', - 'pray', - 'soul', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering' - ], - modifiable: true), - Emoji( - name: 'open hands', - char: '\u{1F450}', - shortName: 'open_hands', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'hand', - 'open', - 'uc6', - 'diversity', - 'body', - 'hands', - 'hi', - 'thank you', - 'condolence', - 'private', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'compassion', - 'прив', - 'privé', - 'privado', - 'reserved' - ]), - Emoji( - name: 'open hands: light skin tone', - char: '\u{1F450}\u{1F3FB}', - shortName: 'open_hands_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'hand', - 'light skin tone', - 'open', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'thank you', - 'condolence', - 'private', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'compassion', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'open hands: medium-light skin tone', - char: '\u{1F450}\u{1F3FC}', - shortName: 'open_hands_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'hand', - 'medium-light skin tone', - 'open', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'thank you', - 'condolence', - 'private', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'compassion', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'open hands: medium skin tone', - char: '\u{1F450}\u{1F3FD}', - shortName: 'open_hands_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'hand', - 'medium skin tone', - 'open', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'thank you', - 'condolence', - 'private', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'compassion', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'open hands: medium-dark skin tone', - char: '\u{1F450}\u{1F3FE}', - shortName: 'open_hands_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'hand', - 'medium-dark skin tone', - 'open', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'thank you', - 'condolence', - 'private', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'compassion', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'open hands: dark skin tone', - char: '\u{1F450}\u{1F3FF}', - shortName: 'open_hands_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'dark skin tone', - 'hand', - 'open', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'thank you', - 'condolence', - 'private', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'compassion', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'raising hands', - char: '\u{1F64C}', - shortName: 'raised_hands', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'celebration', - 'gesture', - 'hand', - 'hooray', - 'raised', - 'uc6', - 'diversity', - 'happy', - 'body', - 'hands', - 'award', - 'hi', - 'thank you', - 'perfect', - 'pray', - 'good', - 'girls night', - 'easter', - 'fame', - 'festivus', - 'language', - 'protest', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'perfecto', - 'perfection', - 'superb', - 'flawless', - 'excellent', - 'supreme', - 'super', - 'great', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'ladies night', - 'girls only', - 'girlfriend', - 'famous', - 'celebrity', - 'blm', - 'demonstration' - ]), - Emoji( - name: 'raising hands: light skin tone', - char: '\u{1F64C}\u{1F3FB}', - shortName: 'raised_hands_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'celebration', - 'gesture', - 'hand', - 'hooray', - 'light skin tone', - 'raised', - 'uc8', - 'diversity', - 'happy', - 'body', - 'hands', - 'award', - 'hi', - 'thank you', - 'perfect', - 'pray', - 'good', - 'girls night', - 'easter', - 'fame', - 'festivus', - 'language', - 'protest', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'perfecto', - 'perfection', - 'superb', - 'flawless', - 'excellent', - 'supreme', - 'super', - 'great', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'ladies night', - 'girls only', - 'girlfriend', - 'famous', - 'celebrity', - 'blm', - 'demonstration' - ], - modifiable: true), - Emoji( - name: 'raising hands: medium-light skin tone', - char: '\u{1F64C}\u{1F3FC}', - shortName: 'raised_hands_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'celebration', - 'gesture', - 'hand', - 'hooray', - 'medium-light skin tone', - 'raised', - 'uc8', - 'diversity', - 'happy', - 'body', - 'hands', - 'award', - 'hi', - 'thank you', - 'perfect', - 'pray', - 'good', - 'girls night', - 'easter', - 'fame', - 'festivus', - 'language', - 'protest', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'perfecto', - 'perfection', - 'superb', - 'flawless', - 'excellent', - 'supreme', - 'super', - 'great', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'ladies night', - 'girls only', - 'girlfriend', - 'famous', - 'celebrity', - 'blm', - 'demonstration' - ], - modifiable: true), - Emoji( - name: 'raising hands: medium skin tone', - char: '\u{1F64C}\u{1F3FD}', - shortName: 'raised_hands_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'celebration', - 'gesture', - 'hand', - 'hooray', - 'medium skin tone', - 'raised', - 'uc8', - 'diversity', - 'happy', - 'body', - 'hands', - 'award', - 'hi', - 'thank you', - 'perfect', - 'pray', - 'good', - 'girls night', - 'easter', - 'fame', - 'festivus', - 'language', - 'protest', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'perfecto', - 'perfection', - 'superb', - 'flawless', - 'excellent', - 'supreme', - 'super', - 'great', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'ladies night', - 'girls only', - 'girlfriend', - 'famous', - 'celebrity', - 'blm', - 'demonstration' - ], - modifiable: true), - Emoji( - name: 'raising hands: medium-dark skin tone', - char: '\u{1F64C}\u{1F3FE}', - shortName: 'raised_hands_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'celebration', - 'gesture', - 'hand', - 'hooray', - 'medium-dark skin tone', - 'raised', - 'uc8', - 'diversity', - 'happy', - 'body', - 'hands', - 'award', - 'hi', - 'thank you', - 'perfect', - 'pray', - 'good', - 'girls night', - 'easter', - 'fame', - 'festivus', - 'language', - 'protest', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'perfecto', - 'perfection', - 'superb', - 'flawless', - 'excellent', - 'supreme', - 'super', - 'great', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'ladies night', - 'girls only', - 'girlfriend', - 'famous', - 'celebrity', - 'blm', - 'demonstration' - ], - modifiable: true), - Emoji( - name: 'raising hands: dark skin tone', - char: '\u{1F64C}\u{1F3FF}', - shortName: 'raised_hands_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'celebration', - 'dark skin tone', - 'gesture', - 'hand', - 'hooray', - 'raised', - 'uc8', - 'diversity', - 'happy', - 'body', - 'hands', - 'award', - 'hi', - 'thank you', - 'perfect', - 'pray', - 'good', - 'girls night', - 'easter', - 'fame', - 'festivus', - 'language', - 'protest', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'perfecto', - 'perfection', - 'superb', - 'flawless', - 'excellent', - 'supreme', - 'super', - 'great', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'ladies night', - 'girls only', - 'girlfriend', - 'famous', - 'celebrity', - 'blm', - 'demonstration' - ], - modifiable: true), - Emoji( - name: 'clapping hands', - char: '\u{1F44F}', - shortName: 'clap', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'clap', - 'hand', - 'uc6', - 'diversity', - 'happy', - 'body', - 'hands', - 'thank you', - 'win', - 'awesome', - 'good', - 'beautiful', - 'clap', - 'pussy', - 'celebrate', - 'pleased', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'clapping', - 'claps', - 'clapping hands', - 'applause', - 'condom', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'please', - 'chill', - 'confident', - 'content' - ]), - Emoji( - name: 'clapping hands: light skin tone', - char: '\u{1F44F}\u{1F3FB}', - shortName: 'clap_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'clap', - 'hand', - 'light skin tone', - 'uc8', - 'diversity', - 'happy', - 'body', - 'hands', - 'thank you', - 'win', - 'awesome', - 'good', - 'beautiful', - 'clap', - 'pussy', - 'celebrate', - 'pleased', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'clapping', - 'claps', - 'clapping hands', - 'applause', - 'condom', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'please', - 'chill', - 'confident', - 'content' - ], - modifiable: true), - Emoji( - name: 'clapping hands: medium-light skin tone', - char: '\u{1F44F}\u{1F3FC}', - shortName: 'clap_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'clap', - 'hand', - 'medium-light skin tone', - 'uc8', - 'diversity', - 'happy', - 'body', - 'hands', - 'thank you', - 'win', - 'awesome', - 'good', - 'beautiful', - 'clap', - 'pussy', - 'celebrate', - 'pleased', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'clapping', - 'claps', - 'clapping hands', - 'applause', - 'condom', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'please', - 'chill', - 'confident', - 'content' - ], - modifiable: true), - Emoji( - name: 'clapping hands: medium skin tone', - char: '\u{1F44F}\u{1F3FD}', - shortName: 'clap_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'clap', - 'hand', - 'medium skin tone', - 'uc8', - 'diversity', - 'happy', - 'body', - 'hands', - 'thank you', - 'win', - 'awesome', - 'good', - 'beautiful', - 'clap', - 'pussy', - 'celebrate', - 'pleased', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'clapping', - 'claps', - 'clapping hands', - 'applause', - 'condom', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'please', - 'chill', - 'confident', - 'content' - ], - modifiable: true), - Emoji( - name: 'clapping hands: medium-dark skin tone', - char: '\u{1F44F}\u{1F3FE}', - shortName: 'clap_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'clap', - 'hand', - 'medium-dark skin tone', - 'uc8', - 'diversity', - 'happy', - 'body', - 'hands', - 'thank you', - 'win', - 'awesome', - 'good', - 'beautiful', - 'clap', - 'pussy', - 'celebrate', - 'pleased', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'clapping', - 'claps', - 'clapping hands', - 'applause', - 'condom', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'please', - 'chill', - 'confident', - 'content' - ], - modifiable: true), - Emoji( - name: 'clapping hands: dark skin tone', - char: '\u{1F44F}\u{1F3FF}', - shortName: 'clap_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'clap', - 'dark skin tone', - 'hand', - 'uc8', - 'diversity', - 'happy', - 'body', - 'hands', - 'thank you', - 'win', - 'awesome', - 'good', - 'beautiful', - 'clap', - 'pussy', - 'celebrate', - 'pleased', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'clapping', - 'claps', - 'clapping hands', - 'applause', - 'condom', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'please', - 'chill', - 'confident', - 'content' - ], - modifiable: true), - Emoji( - name: 'handshake', - char: '\u{1F91D}', - shortName: 'handshake', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'agreement', - 'hand', - 'handshake', - 'meeting', - 'shake', - 'uc9', - 'diversity', - 'body', - 'hands', - 'hi', - 'business', - 'proud', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle' - ]), - Emoji( - name: 'thumbs up', - char: '\u{1F44D}', - shortName: 'thumbsup', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - '+1', - 'hand', - 'thumb', - 'up', - 'uc6', - 'diversity', - 'happy', - 'body', - 'hands', - 'award', - 'hi', - 'luck', - 'thank you', - 'perfect', - 'awesome', - 'good', - 'beautiful', - 'correct', - 'fun', - 'proud', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'good luck', - 'lucky', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'perfecto', - 'perfection', - 'superb', - 'flawless', - 'excellent', - 'supreme', - 'super', - 'great', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'passing grade', - '(y)' - ]), - Emoji( - name: 'thumbs up: light skin tone', - char: '\u{1F44D}\u{1F3FB}', - shortName: 'thumbsup_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - '+1', - 'hand', - 'light skin tone', - 'thumb', - 'up', - 'uc8', - 'diversity', - 'happy', - 'body', - 'hands', - 'award', - 'hi', - 'luck', - 'thank you', - 'perfect', - 'awesome', - 'good', - 'beautiful', - 'correct', - 'fun', - 'proud', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'good luck', - 'lucky', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'perfecto', - 'perfection', - 'superb', - 'flawless', - 'excellent', - 'supreme', - 'super', - 'great', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'passing grade' - ], - modifiable: true), - Emoji( - name: 'thumbs up: medium-light skin tone', - char: '\u{1F44D}\u{1F3FC}', - shortName: 'thumbsup_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - '+1', - 'hand', - 'medium-light skin tone', - 'thumb', - 'up', - 'uc8', - 'diversity', - 'happy', - 'body', - 'hands', - 'award', - 'hi', - 'luck', - 'thank you', - 'perfect', - 'awesome', - 'good', - 'beautiful', - 'correct', - 'fun', - 'proud', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'good luck', - 'lucky', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'perfecto', - 'perfection', - 'superb', - 'flawless', - 'excellent', - 'supreme', - 'super', - 'great', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'passing grade' - ], - modifiable: true), - Emoji( - name: 'thumbs up: medium skin tone', - char: '\u{1F44D}\u{1F3FD}', - shortName: 'thumbsup_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - '+1', - 'hand', - 'medium skin tone', - 'thumb', - 'up', - 'uc8', - 'diversity', - 'happy', - 'body', - 'hands', - 'award', - 'hi', - 'luck', - 'thank you', - 'perfect', - 'awesome', - 'good', - 'beautiful', - 'correct', - 'fun', - 'proud', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'good luck', - 'lucky', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'perfecto', - 'perfection', - 'superb', - 'flawless', - 'excellent', - 'supreme', - 'super', - 'great', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'passing grade' - ], - modifiable: true), - Emoji( - name: 'thumbs up: medium-dark skin tone', - char: '\u{1F44D}\u{1F3FE}', - shortName: 'thumbsup_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - '+1', - 'hand', - 'medium-dark skin tone', - 'thumb', - 'up', - 'uc8', - 'diversity', - 'happy', - 'body', - 'hands', - 'award', - 'hi', - 'luck', - 'thank you', - 'perfect', - 'awesome', - 'good', - 'beautiful', - 'correct', - 'fun', - 'proud', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'good luck', - 'lucky', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'perfecto', - 'perfection', - 'superb', - 'flawless', - 'excellent', - 'supreme', - 'super', - 'great', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'passing grade' - ], - modifiable: true), - Emoji( - name: 'thumbs up: dark skin tone', - char: '\u{1F44D}\u{1F3FF}', - shortName: 'thumbsup_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - '+1', - 'dark skin tone', - 'hand', - 'thumb', - 'up', - 'uc8', - 'diversity', - 'happy', - 'body', - 'hands', - 'award', - 'hi', - 'luck', - 'thank you', - 'perfect', - 'awesome', - 'good', - 'beautiful', - 'correct', - 'fun', - 'proud', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'good luck', - 'lucky', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'perfecto', - 'perfection', - 'superb', - 'flawless', - 'excellent', - 'supreme', - 'super', - 'great', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'passing grade' - ], - modifiable: true), - Emoji( - name: 'thumbs down', - char: '\u{1F44E}', - shortName: 'thumbsdown', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - '-1', - 'down', - 'hand', - 'thumb', - 'uc6', - 'diversity', - 'sad', - 'body', - 'hands', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers' - ]), - Emoji( - name: 'thumbs down: light skin tone', - char: '\u{1F44E}\u{1F3FB}', - shortName: 'thumbsdown_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - '-1', - 'down', - 'hand', - 'light skin tone', - 'thumb', - 'uc8', - 'diversity', - 'sad', - 'body', - 'hands', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers' - ], - modifiable: true), - Emoji( - name: 'thumbs down: medium-light skin tone', - char: '\u{1F44E}\u{1F3FC}', - shortName: 'thumbsdown_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - '-1', - 'down', - 'hand', - 'medium-light skin tone', - 'thumb', - 'uc8', - 'diversity', - 'sad', - 'body', - 'hands', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers' - ], - modifiable: true), - Emoji( - name: 'thumbs down: medium skin tone', - char: '\u{1F44E}\u{1F3FD}', - shortName: 'thumbsdown_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - '-1', - 'down', - 'hand', - 'medium skin tone', - 'thumb', - 'uc8', - 'diversity', - 'sad', - 'body', - 'hands', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers' - ], - modifiable: true), - Emoji( - name: 'thumbs down: medium-dark skin tone', - char: '\u{1F44E}\u{1F3FE}', - shortName: 'thumbsdown_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - '-1', - 'down', - 'hand', - 'medium-dark skin tone', - 'thumb', - 'uc8', - 'diversity', - 'sad', - 'body', - 'hands', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers' - ], - modifiable: true), - Emoji( - name: 'thumbs down: dark skin tone', - char: '\u{1F44E}\u{1F3FF}', - shortName: 'thumbsdown_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - '-1', - 'dark skin tone', - 'down', - 'hand', - 'thumb', - 'uc8', - 'diversity', - 'sad', - 'body', - 'hands', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers' - ], - modifiable: true), - Emoji( - name: 'oncoming fist', - char: '\u{1F44A}', - shortName: 'punch', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - 'clenched', - 'fist', - 'hand', - 'punch', - 'uc6', - 'diversity', - 'body', - 'hands', - 'hi', - 'fist bump', - 'win', - 'awesome', - 'boys night', - 'friend', - 'fight', - 'hit', - 'proud', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fist', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'guys night', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'punch', - 'pow', - 'bam' - ]), - Emoji( - name: 'oncoming fist: light skin tone', - char: '\u{1F44A}\u{1F3FB}', - shortName: 'punch_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - 'clenched', - 'fist', - 'hand', - 'light skin tone', - 'punch', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'fist bump', - 'win', - 'awesome', - 'boys night', - 'friend', - 'fight', - 'hit', - 'proud', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fist', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'guys night', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'punch', - 'pow', - 'bam' - ], - modifiable: true), - Emoji( - name: 'oncoming fist: medium-light skin tone', - char: '\u{1F44A}\u{1F3FC}', - shortName: 'punch_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - 'clenched', - 'fist', - 'hand', - 'medium-light skin tone', - 'punch', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'fist bump', - 'win', - 'awesome', - 'boys night', - 'friend', - 'fight', - 'hit', - 'proud', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fist', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'guys night', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'punch', - 'pow', - 'bam' - ], - modifiable: true), - Emoji( - name: 'oncoming fist: medium skin tone', - char: '\u{1F44A}\u{1F3FD}', - shortName: 'punch_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - 'clenched', - 'fist', - 'hand', - 'medium skin tone', - 'punch', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'fist bump', - 'win', - 'awesome', - 'boys night', - 'friend', - 'fight', - 'hit', - 'proud', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fist', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'guys night', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'punch', - 'pow', - 'bam' - ], - modifiable: true), - Emoji( - name: 'oncoming fist: medium-dark skin tone', - char: '\u{1F44A}\u{1F3FE}', - shortName: 'punch_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - 'clenched', - 'fist', - 'hand', - 'medium-dark skin tone', - 'punch', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'fist bump', - 'win', - 'awesome', - 'boys night', - 'friend', - 'fight', - 'hit', - 'proud', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fist', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'guys night', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'punch', - 'pow', - 'bam' - ], - modifiable: true), - Emoji( - name: 'oncoming fist: dark skin tone', - char: '\u{1F44A}\u{1F3FF}', - shortName: 'punch_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - 'clenched', - 'dark skin tone', - 'fist', - 'hand', - 'punch', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'fist bump', - 'win', - 'awesome', - 'boys night', - 'friend', - 'fight', - 'hit', - 'proud', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fist', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'guys night', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'punch', - 'pow', - 'bam' - ], - modifiable: true), - Emoji( - name: 'raised fist', - char: '\u{270A}', - shortName: 'fist', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - 'clenched', - 'fist', - 'hand', - 'punch', - 'uc6', - 'diversity', - 'body', - 'hands', - 'hi', - 'fist bump', - 'win', - 'condolence', - 'proud', - 'language', - 'protest', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fist', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'compassion', - 'blm', - 'demonstration' - ]), - Emoji( - name: 'raised fist: light skin tone', - char: '\u{270A}\u{1F3FB}', - shortName: 'fist_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - 'clenched', - 'fist', - 'hand', - 'light skin tone', - 'punch', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'fist bump', - 'win', - 'condolence', - 'proud', - 'language', - 'protest', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fist', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'compassion', - 'blm', - 'demonstration' - ], - modifiable: true), - Emoji( - name: 'raised fist: medium-light skin tone', - char: '\u{270A}\u{1F3FC}', - shortName: 'fist_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - 'clenched', - 'fist', - 'hand', - 'medium-light skin tone', - 'punch', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'fist bump', - 'win', - 'condolence', - 'proud', - 'language', - 'protest', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fist', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'compassion', - 'blm', - 'demonstration' - ], - modifiable: true), - Emoji( - name: 'raised fist: medium skin tone', - char: '\u{270A}\u{1F3FD}', - shortName: 'fist_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - 'clenched', - 'fist', - 'hand', - 'medium skin tone', - 'punch', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'fist bump', - 'win', - 'condolence', - 'proud', - 'language', - 'protest', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fist', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'compassion', - 'blm', - 'demonstration' - ], - modifiable: true), - Emoji( - name: 'raised fist: medium-dark skin tone', - char: '\u{270A}\u{1F3FE}', - shortName: 'fist_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - 'clenched', - 'fist', - 'hand', - 'medium-dark skin tone', - 'punch', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'fist bump', - 'win', - 'condolence', - 'proud', - 'language', - 'protest', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fist', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'compassion', - 'blm', - 'demonstration' - ], - modifiable: true), - Emoji( - name: 'raised fist: dark skin tone', - char: '\u{270A}\u{1F3FF}', - shortName: 'fist_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - 'clenched', - 'dark skin tone', - 'fist', - 'hand', - 'punch', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'fist bump', - 'win', - 'condolence', - 'proud', - 'language', - 'protest', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fist', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'compassion', - 'blm', - 'demonstration' - ], - modifiable: true), - Emoji( - name: 'left-facing fist', - char: '\u{1F91B}', - shortName: 'left_facing_fist', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - 'fist', - 'leftwards', - 'uc9', - 'diversity', - 'body', - 'hands', - 'hi', - 'fist bump', - 'win', - 'friend', - 'hit', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fist', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'punch', - 'pow', - 'bam' - ]), - Emoji( - name: 'left-facing fist: light skin tone', - char: '\u{1F91B}\u{1F3FB}', - shortName: 'left_facing_fist_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - 'fist', - 'leftwards', - 'light skin tone', - 'uc9', - 'diversity', - 'body', - 'hands', - 'hi', - 'fist bump', - 'win', - 'friend', - 'hit', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fist', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'punch', - 'pow', - 'bam' - ], - modifiable: true), - Emoji( - name: 'left-facing fist: medium-light skin tone', - char: '\u{1F91B}\u{1F3FC}', - shortName: 'left_facing_fist_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - 'fist', - 'leftwards', - 'medium-light skin tone', - 'uc9', - 'diversity', - 'body', - 'hands', - 'hi', - 'fist bump', - 'win', - 'friend', - 'hit', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fist', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'punch', - 'pow', - 'bam' - ], - modifiable: true), - Emoji( - name: 'left-facing fist: medium skin tone', - char: '\u{1F91B}\u{1F3FD}', - shortName: 'left_facing_fist_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - 'fist', - 'leftwards', - 'medium skin tone', - 'uc9', - 'diversity', - 'body', - 'hands', - 'hi', - 'fist bump', - 'win', - 'friend', - 'hit', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fist', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'punch', - 'pow', - 'bam' - ], - modifiable: true), - Emoji( - name: 'left-facing fist: medium-dark skin tone', - char: '\u{1F91B}\u{1F3FE}', - shortName: 'left_facing_fist_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - 'fist', - 'leftwards', - 'medium-dark skin tone', - 'uc9', - 'diversity', - 'body', - 'hands', - 'hi', - 'fist bump', - 'win', - 'friend', - 'hit', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fist', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'punch', - 'pow', - 'bam' - ], - modifiable: true), - Emoji( - name: 'left-facing fist: dark skin tone', - char: '\u{1F91B}\u{1F3FF}', - shortName: 'left_facing_fist_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - 'dark skin tone', - 'fist', - 'leftwards', - 'uc9', - 'diversity', - 'body', - 'hands', - 'hi', - 'fist bump', - 'win', - 'friend', - 'hit', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fist', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'punch', - 'pow', - 'bam' - ], - modifiable: true), - Emoji( - name: 'right-facing fist', - char: '\u{1F91C}', - shortName: 'right_facing_fist', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - 'fist', - 'rightwards', - 'uc9', - 'diversity', - 'body', - 'hands', - 'hi', - 'fist bump', - 'friend', - 'hit', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fist', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'punch', - 'pow', - 'bam' - ]), - Emoji( - name: 'right-facing fist: light skin tone', - char: '\u{1F91C}\u{1F3FB}', - shortName: 'right_facing_fist_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - 'fist', - 'light skin tone', - 'rightwards', - 'uc9', - 'diversity', - 'body', - 'hands', - 'hi', - 'fist bump', - 'friend', - 'hit', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fist', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'punch', - 'pow', - 'bam' - ], - modifiable: true), - Emoji( - name: 'right-facing fist: medium-light skin tone', - char: '\u{1F91C}\u{1F3FC}', - shortName: 'right_facing_fist_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - 'fist', - 'medium-light skin tone', - 'rightwards', - 'uc9', - 'diversity', - 'body', - 'hands', - 'hi', - 'fist bump', - 'friend', - 'hit', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fist', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'punch', - 'pow', - 'bam' - ], - modifiable: true), - Emoji( - name: 'right-facing fist: medium skin tone', - char: '\u{1F91C}\u{1F3FD}', - shortName: 'right_facing_fist_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - 'fist', - 'medium skin tone', - 'rightwards', - 'uc9', - 'diversity', - 'body', - 'hands', - 'hi', - 'fist bump', - 'friend', - 'hit', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fist', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'punch', - 'pow', - 'bam' - ], - modifiable: true), - Emoji( - name: 'right-facing fist: medium-dark skin tone', - char: '\u{1F91C}\u{1F3FE}', - shortName: 'right_facing_fist_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - 'fist', - 'medium-dark skin tone', - 'rightwards', - 'uc9', - 'diversity', - 'body', - 'hands', - 'hi', - 'fist bump', - 'friend', - 'hit', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fist', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'punch', - 'pow', - 'bam' - ], - modifiable: true), - Emoji( - name: 'right-facing fist: dark skin tone', - char: '\u{1F91C}\u{1F3FF}', - shortName: 'right_facing_fist_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersClosed, - keywords: [ - 'dark skin tone', - 'fist', - 'rightwards', - 'uc9', - 'diversity', - 'body', - 'hands', - 'hi', - 'fist bump', - 'friend', - 'hit', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fist', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'punch', - 'pow', - 'bam' - ], - modifiable: true), - Emoji( - name: 'crossed fingers', - char: '\u{1F91E}', - shortName: 'fingers_crossed', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'cross', - 'finger', - 'hand', - 'luck', - 'uc9', - 'diversity', - 'body', - 'hands', - 'donald trump', - 'irish', - 'hope', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'trump', - 'saint patricks day', - 'st patricks day', - 'leprechaun', - 'swear', - 'promise' - ]), - Emoji( - name: 'crossed fingers: light skin tone', - char: '\u{1F91E}\u{1F3FB}', - shortName: 'fingers_crossed_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'cross', - 'finger', - 'hand', - 'light skin tone', - 'luck', - 'uc9', - 'diversity', - 'body', - 'hands', - 'donald trump', - 'irish', - 'hope', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'trump', - 'saint patricks day', - 'st patricks day', - 'leprechaun', - 'swear', - 'promise' - ], - modifiable: true), - Emoji( - name: 'crossed fingers: medium-light skin tone', - char: '\u{1F91E}\u{1F3FC}', - shortName: 'fingers_crossed_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'cross', - 'finger', - 'hand', - 'luck', - 'medium-light skin tone', - 'uc9', - 'diversity', - 'body', - 'hands', - 'donald trump', - 'irish', - 'hope', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'trump', - 'saint patricks day', - 'st patricks day', - 'leprechaun', - 'swear', - 'promise' - ], - modifiable: true), - Emoji( - name: 'crossed fingers: medium skin tone', - char: '\u{1F91E}\u{1F3FD}', - shortName: 'fingers_crossed_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'cross', - 'finger', - 'hand', - 'luck', - 'medium skin tone', - 'uc9', - 'diversity', - 'body', - 'hands', - 'donald trump', - 'irish', - 'hope', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'trump', - 'saint patricks day', - 'st patricks day', - 'leprechaun', - 'swear', - 'promise' - ], - modifiable: true), - Emoji( - name: 'crossed fingers: medium-dark skin tone', - char: '\u{1F91E}\u{1F3FE}', - shortName: 'fingers_crossed_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'cross', - 'finger', - 'hand', - 'luck', - 'medium-dark skin tone', - 'uc9', - 'diversity', - 'body', - 'hands', - 'donald trump', - 'irish', - 'hope', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'trump', - 'saint patricks day', - 'st patricks day', - 'leprechaun', - 'swear', - 'promise' - ], - modifiable: true), - Emoji( - name: 'crossed fingers: dark skin tone', - char: '\u{1F91E}\u{1F3FF}', - shortName: 'fingers_crossed_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'cross', - 'dark skin tone', - 'finger', - 'hand', - 'luck', - 'uc9', - 'diversity', - 'body', - 'hands', - 'donald trump', - 'irish', - 'hope', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'trump', - 'saint patricks day', - 'st patricks day', - 'leprechaun', - 'swear', - 'promise' - ], - modifiable: true), - Emoji( - name: 'victory hand', - char: '\u{270C}\u{FE0F}', - shortName: 'v', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'hand', - 'v', - 'victory', - 'uc1', - 'diversity', - 'peace', - 'body', - 'hands', - 'hi', - 'thank you', - 'girls night', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'peace out', - 'peace sign', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'ladies night', - 'girls only', - 'girlfriend' - ]), - Emoji( - name: 'victory hand: light skin tone', - char: '\u{270C}\u{1F3FB}', - shortName: 'v_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'hand', - 'light skin tone', - 'v', - 'victory', - 'uc8', - 'diversity', - 'peace', - 'body', - 'hands', - 'hi', - 'thank you', - 'girls night', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'peace out', - 'peace sign', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'ladies night', - 'girls only', - 'girlfriend' - ], - modifiable: true), - Emoji( - name: 'victory hand: medium-light skin tone', - char: '\u{270C}\u{1F3FC}', - shortName: 'v_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'hand', - 'medium-light skin tone', - 'v', - 'victory', - 'uc8', - 'diversity', - 'peace', - 'body', - 'hands', - 'hi', - 'thank you', - 'girls night', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'peace out', - 'peace sign', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'ladies night', - 'girls only', - 'girlfriend' - ], - modifiable: true), - Emoji( - name: 'victory hand: medium skin tone', - char: '\u{270C}\u{1F3FD}', - shortName: 'v_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'hand', - 'medium skin tone', - 'v', - 'victory', - 'uc8', - 'diversity', - 'peace', - 'body', - 'hands', - 'hi', - 'thank you', - 'girls night', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'peace out', - 'peace sign', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'ladies night', - 'girls only', - 'girlfriend' - ], - modifiable: true), - Emoji( - name: 'victory hand: medium-dark skin tone', - char: '\u{270C}\u{1F3FE}', - shortName: 'v_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'hand', - 'medium-dark skin tone', - 'v', - 'victory', - 'uc8', - 'diversity', - 'peace', - 'body', - 'hands', - 'hi', - 'thank you', - 'girls night', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'peace out', - 'peace sign', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'ladies night', - 'girls only', - 'girlfriend' - ], - modifiable: true), - Emoji( - name: 'victory hand: dark skin tone', - char: '\u{270C}\u{1F3FF}', - shortName: 'v_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'dark skin tone', - 'hand', - 'v', - 'victory', - 'uc8', - 'diversity', - 'peace', - 'body', - 'hands', - 'hi', - 'thank you', - 'girls night', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'peace out', - 'peace sign', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'ladies night', - 'girls only', - 'girlfriend' - ], - modifiable: true), - Emoji( - name: 'love-you gesture', - char: '\u{1F91F}', - shortName: 'love_you_gesture', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'ILY', - 'hand', - 'uc10', - 'diversity', - 'body', - 'hands', - 'love', - 'beautiful', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely' - ]), - Emoji( - name: 'love-you gesture: light skin tone', - char: '\u{1F91F}\u{1F3FB}', - shortName: 'love_you_gesture_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'ILY', - 'hand', - 'light skin tone', - 'uc10', - 'diversity', - 'body', - 'hands', - 'love', - 'beautiful', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely' - ], - modifiable: true), - Emoji( - name: 'love-you gesture: medium-light skin tone', - char: '\u{1F91F}\u{1F3FC}', - shortName: 'love_you_gesture_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'ILY', - 'hand', - 'medium-light skin tone', - 'uc10', - 'diversity', - 'body', - 'hands', - 'love', - 'beautiful', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely' - ], - modifiable: true), - Emoji( - name: 'love-you gesture: medium skin tone', - char: '\u{1F91F}\u{1F3FD}', - shortName: 'love_you_gesture_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'ILY', - 'hand', - 'medium skin tone', - 'uc10', - 'diversity', - 'body', - 'hands', - 'love', - 'beautiful', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely' - ], - modifiable: true), - Emoji( - name: 'love-you gesture: medium-dark skin tone', - char: '\u{1F91F}\u{1F3FE}', - shortName: 'love_you_gesture_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'ILY', - 'hand', - 'medium-dark skin tone', - 'uc10', - 'diversity', - 'body', - 'hands', - 'love', - 'beautiful', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely' - ], - modifiable: true), - Emoji( - name: 'love-you gesture: dark skin tone', - char: '\u{1F91F}\u{1F3FF}', - shortName: 'love_you_gesture_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'ILY', - 'dark skin tone', - 'hand', - 'uc10', - 'diversity', - 'body', - 'hands', - 'love', - 'beautiful', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely' - ], - modifiable: true), - Emoji( - name: 'sign of the horns', - char: '\u{1F918}', - shortName: 'metal', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'finger', - 'hand', - 'horns', - 'rock-on', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'boys night', - 'rock and roll', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'guys night' - ]), - Emoji( - name: 'sign of the horns: light skin tone', - char: '\u{1F918}\u{1F3FB}', - shortName: 'metal_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'finger', - 'hand', - 'horns', - 'light skin tone', - 'rock-on', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'boys night', - 'rock and roll', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'guys night' - ], - modifiable: true), - Emoji( - name: 'sign of the horns: medium-light skin tone', - char: '\u{1F918}\u{1F3FC}', - shortName: 'metal_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'finger', - 'hand', - 'horns', - 'medium-light skin tone', - 'rock-on', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'boys night', - 'rock and roll', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'guys night' - ], - modifiable: true), - Emoji( - name: 'sign of the horns: medium skin tone', - char: '\u{1F918}\u{1F3FD}', - shortName: 'metal_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'finger', - 'hand', - 'horns', - 'medium skin tone', - 'rock-on', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'boys night', - 'rock and roll', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'guys night' - ], - modifiable: true), - Emoji( - name: 'sign of the horns: medium-dark skin tone', - char: '\u{1F918}\u{1F3FE}', - shortName: 'metal_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'finger', - 'hand', - 'horns', - 'medium-dark skin tone', - 'rock-on', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'boys night', - 'rock and roll', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'guys night' - ], - modifiable: true), - Emoji( - name: 'sign of the horns: dark skin tone', - char: '\u{1F918}\u{1F3FF}', - shortName: 'metal_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'dark skin tone', - 'finger', - 'hand', - 'horns', - 'rock-on', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'boys night', - 'rock and roll', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'guys night' - ], - modifiable: true), - Emoji( - name: 'OK hand', - char: '\u{1F44C}', - shortName: 'ok_hand', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'OK', - 'hand', - 'uc6', - 'diversity', - 'happy', - 'butt', - 'body', - 'hands', - 'hi', - 'sex', - 'thank you', - 'perfect', - 'awesome', - 'good', - 'beautiful', - 'google', - 'correct', - 'porn', - 'fun', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'ass', - 'booty', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fuck', - 'fucking', - 'horny', - 'humping', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'perfecto', - 'perfection', - 'superb', - 'flawless', - 'excellent', - 'supreme', - 'super', - 'great', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'passing grade' - ]), - Emoji( - name: 'OK hand: light skin tone', - char: '\u{1F44C}\u{1F3FB}', - shortName: 'ok_hand_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'OK', - 'hand', - 'light skin tone', - 'uc8', - 'diversity', - 'happy', - 'butt', - 'body', - 'hands', - 'hi', - 'sex', - 'thank you', - 'perfect', - 'awesome', - 'good', - 'beautiful', - 'google', - 'correct', - 'porn', - 'fun', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'ass', - 'booty', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fuck', - 'fucking', - 'horny', - 'humping', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'perfecto', - 'perfection', - 'superb', - 'flawless', - 'excellent', - 'supreme', - 'super', - 'great', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'passing grade' - ], - modifiable: true), - Emoji( - name: 'OK hand: medium-light skin tone', - char: '\u{1F44C}\u{1F3FC}', - shortName: 'ok_hand_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'OK', - 'hand', - 'medium-light skin tone', - 'uc8', - 'diversity', - 'happy', - 'butt', - 'body', - 'hands', - 'hi', - 'sex', - 'thank you', - 'perfect', - 'awesome', - 'good', - 'beautiful', - 'google', - 'correct', - 'porn', - 'fun', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'ass', - 'booty', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fuck', - 'fucking', - 'horny', - 'humping', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'perfecto', - 'perfection', - 'superb', - 'flawless', - 'excellent', - 'supreme', - 'super', - 'great', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'passing grade' - ], - modifiable: true), - Emoji( - name: 'OK hand: medium skin tone', - char: '\u{1F44C}\u{1F3FD}', - shortName: 'ok_hand_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'OK', - 'hand', - 'medium skin tone', - 'uc8', - 'diversity', - 'happy', - 'butt', - 'body', - 'hands', - 'hi', - 'sex', - 'thank you', - 'perfect', - 'awesome', - 'good', - 'beautiful', - 'google', - 'correct', - 'porn', - 'fun', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'ass', - 'booty', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fuck', - 'fucking', - 'horny', - 'humping', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'perfecto', - 'perfection', - 'superb', - 'flawless', - 'excellent', - 'supreme', - 'super', - 'great', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'passing grade' - ], - modifiable: true), - Emoji( - name: 'OK hand: medium-dark skin tone', - char: '\u{1F44C}\u{1F3FE}', - shortName: 'ok_hand_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'OK', - 'hand', - 'medium-dark skin tone', - 'uc8', - 'diversity', - 'happy', - 'butt', - 'body', - 'hands', - 'hi', - 'sex', - 'thank you', - 'perfect', - 'awesome', - 'good', - 'beautiful', - 'google', - 'correct', - 'porn', - 'fun', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'ass', - 'booty', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fuck', - 'fucking', - 'horny', - 'humping', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'perfecto', - 'perfection', - 'superb', - 'flawless', - 'excellent', - 'supreme', - 'super', - 'great', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'passing grade' - ], - modifiable: true), - Emoji( - name: 'OK hand: dark skin tone', - char: '\u{1F44C}\u{1F3FF}', - shortName: 'ok_hand_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'OK', - 'dark skin tone', - 'hand', - 'uc8', - 'diversity', - 'happy', - 'butt', - 'body', - 'hands', - 'hi', - 'sex', - 'thank you', - 'perfect', - 'awesome', - 'good', - 'beautiful', - 'google', - 'correct', - 'porn', - 'fun', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'ass', - 'booty', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fuck', - 'fucking', - 'horny', - 'humping', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'perfecto', - 'perfection', - 'superb', - 'flawless', - 'excellent', - 'supreme', - 'super', - 'great', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'passing grade' - ], - modifiable: true), - Emoji( - name: 'pinching hand', - char: '\u{1F90F}', - shortName: 'pinching_hand', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'uc12', - 'diversity', - 'penis', - 'body', - 'hands', - 'donald trump', - 'half', - 'quiet', - 'tiny', - 'greed', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'dick', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'trump', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'petite bite', - 'small dick', - 'small', - 'selfish' - ]), - Emoji( - name: 'pinching hand: light skin tone', - char: '\u{1F90F}\u{1F3FB}', - shortName: 'pinching_hand_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'uc12', - 'diversity', - 'penis', - 'body', - 'hands', - 'donald trump', - 'half', - 'quiet', - 'tiny', - 'greed', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'dick', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'trump', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'petite bite', - 'small dick', - 'small', - 'selfish' - ], - modifiable: true), - Emoji( - name: 'pinching hand: medium-light skin tone', - char: '\u{1F90F}\u{1F3FC}', - shortName: 'pinching_hand_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'uc12', - 'diversity', - 'penis', - 'body', - 'hands', - 'donald trump', - 'half', - 'quiet', - 'tiny', - 'greed', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'dick', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'trump', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'petite bite', - 'small dick', - 'small', - 'selfish' - ], - modifiable: true), - Emoji( - name: 'pinching hand: medium skin tone', - char: '\u{1F90F}\u{1F3FD}', - shortName: 'pinching_hand_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'uc12', - 'diversity', - 'penis', - 'body', - 'hands', - 'donald trump', - 'half', - 'quiet', - 'tiny', - 'greed', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'dick', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'trump', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'petite bite', - 'small dick', - 'small', - 'selfish' - ], - modifiable: true), - Emoji( - name: 'pinching hand: medium-dark skin tone', - char: '\u{1F90F}\u{1F3FE}', - shortName: 'pinching_hand_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'uc12', - 'diversity', - 'penis', - 'body', - 'hands', - 'donald trump', - 'half', - 'quiet', - 'tiny', - 'greed', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'dick', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'trump', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'petite bite', - 'small dick', - 'small', - 'selfish' - ], - modifiable: true), - Emoji( - name: 'pinching hand: dark skin tone', - char: '\u{1F90F}\u{1F3FF}', - shortName: 'pinching_hand_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'uc12', - 'diversity', - 'penis', - 'body', - 'hands', - 'donald trump', - 'half', - 'quiet', - 'tiny', - 'greed', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'dick', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'trump', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'petite bite', - 'small dick', - 'small', - 'selfish' - ], - modifiable: true), - Emoji( - name: 'pinched fingers', - char: '\u{1F90C}', - shortName: 'pinched_fingers', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'uc13', - 'diversity', - 'italian', - 'body', - 'hands', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'italy', - 'italie', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers' - ]), - Emoji( - name: 'pinched fingers: medium-light skin tone', - char: '\u{1F90C}\u{1F3FC}', - shortName: 'pinched_fingers_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'uc13', - 'diversity', - 'italian', - 'body', - 'hands', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'italy', - 'italie', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers' - ], - modifiable: true), - Emoji( - name: 'pinched fingers: light skin tone', - char: '\u{1F90C}\u{1F3FB}', - shortName: 'pinched_fingers_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'uc13', - 'diversity', - 'italian', - 'body', - 'hands', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'italy', - 'italie', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers' - ], - modifiable: true), - Emoji( - name: 'pinched fingers: medium skin tone', - char: '\u{1F90C}\u{1F3FD}', - shortName: 'pinched_fingers_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'uc13', - 'diversity', - 'italian', - 'body', - 'hands', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'italy', - 'italie', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers' - ], - modifiable: true), - Emoji( - name: 'pinched fingers: medium-dark skin tone', - char: '\u{1F90C}\u{1F3FE}', - shortName: 'pinched_fingers_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'uc13', - 'diversity', - 'italian', - 'body', - 'hands', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'italy', - 'italie', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers' - ], - modifiable: true), - Emoji( - name: 'pinched fingers: dark skin tone', - char: '\u{1F90C}\u{1F3FF}', - shortName: 'pinched_fingers_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'uc13', - 'diversity', - 'italian', - 'body', - 'hands', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'italy', - 'italie', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers' - ], - modifiable: true), - Emoji( - name: 'backhand index pointing left', - char: '\u{1F448}', - shortName: 'point_left', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'backhand', - 'finger', - 'hand', - 'index', - 'point', - 'uc6', - 'diversity', - 'body', - 'hands', - 'hi', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle' - ]), - Emoji( - name: 'backhand index pointing left: light skin tone', - char: '\u{1F448}\u{1F3FB}', - shortName: 'point_left_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'backhand', - 'finger', - 'hand', - 'index', - 'light skin tone', - 'point', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle' - ], - modifiable: true), - Emoji( - name: 'backhand index pointing left: medium-light skin tone', - char: '\u{1F448}\u{1F3FC}', - shortName: 'point_left_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'backhand', - 'finger', - 'hand', - 'index', - 'medium-light skin tone', - 'point', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle' - ], - modifiable: true), - Emoji( - name: 'backhand index pointing left: medium skin tone', - char: '\u{1F448}\u{1F3FD}', - shortName: 'point_left_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'backhand', - 'finger', - 'hand', - 'index', - 'medium skin tone', - 'point', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle' - ], - modifiable: true), - Emoji( - name: 'backhand index pointing left: medium-dark skin tone', - char: '\u{1F448}\u{1F3FE}', - shortName: 'point_left_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'backhand', - 'finger', - 'hand', - 'index', - 'medium-dark skin tone', - 'point', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle' - ], - modifiable: true), - Emoji( - name: 'backhand index pointing left: dark skin tone', - char: '\u{1F448}\u{1F3FF}', - shortName: 'point_left_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'backhand', - 'dark skin tone', - 'finger', - 'hand', - 'index', - 'point', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle' - ], - modifiable: true), - Emoji( - name: 'backhand index pointing right', - char: '\u{1F449}', - shortName: 'point_right', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'backhand', - 'finger', - 'hand', - 'index', - 'point', - 'uc6', - 'diversity', - 'body', - 'hands', - 'hi', - 'sex', - 'download', - 'porn', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fuck', - 'fucking', - 'horny', - 'humping' - ]), - Emoji( - name: 'backhand index pointing right: light skin tone', - char: '\u{1F449}\u{1F3FB}', - shortName: 'point_right_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'backhand', - 'finger', - 'hand', - 'index', - 'light skin tone', - 'point', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'sex', - 'download', - 'porn', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fuck', - 'fucking', - 'horny', - 'humping' - ], - modifiable: true), - Emoji( - name: 'backhand index pointing right: medium-light skin tone', - char: '\u{1F449}\u{1F3FC}', - shortName: 'point_right_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'backhand', - 'finger', - 'hand', - 'index', - 'medium-light skin tone', - 'point', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'sex', - 'download', - 'porn', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fuck', - 'fucking', - 'horny', - 'humping' - ], - modifiable: true), - Emoji( - name: 'backhand index pointing right: medium skin tone', - char: '\u{1F449}\u{1F3FD}', - shortName: 'point_right_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'backhand', - 'finger', - 'hand', - 'index', - 'medium skin tone', - 'point', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'sex', - 'download', - 'porn', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fuck', - 'fucking', - 'horny', - 'humping' - ], - modifiable: true), - Emoji( - name: 'backhand index pointing right: medium-dark skin tone', - char: '\u{1F449}\u{1F3FE}', - shortName: 'point_right_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'backhand', - 'finger', - 'hand', - 'index', - 'medium-dark skin tone', - 'point', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'sex', - 'download', - 'porn', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fuck', - 'fucking', - 'horny', - 'humping' - ], - modifiable: true), - Emoji( - name: 'backhand index pointing right: dark skin tone', - char: '\u{1F449}\u{1F3FF}', - shortName: 'point_right_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'backhand', - 'dark skin tone', - 'finger', - 'hand', - 'index', - 'point', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'sex', - 'download', - 'porn', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'fuck', - 'fucking', - 'horny', - 'humping' - ], - modifiable: true), - Emoji( - name: 'backhand index pointing up', - char: '\u{1F446}', - shortName: 'point_up_2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'backhand', - 'finger', - 'hand', - 'index', - 'point', - 'up', - 'uc6', - 'diversity', - 'body', - 'hands', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers' - ]), - Emoji( - name: 'backhand index pointing up: light skin tone', - char: '\u{1F446}\u{1F3FB}', - shortName: 'point_up_2_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'backhand', - 'finger', - 'hand', - 'index', - 'light skin tone', - 'point', - 'up', - 'uc8', - 'diversity', - 'body', - 'hands', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers' - ], - modifiable: true), - Emoji( - name: 'backhand index pointing up: medium-light skin tone', - char: '\u{1F446}\u{1F3FC}', - shortName: 'point_up_2_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'backhand', - 'finger', - 'hand', - 'index', - 'medium-light skin tone', - 'point', - 'up', - 'uc8', - 'diversity', - 'body', - 'hands', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers' - ], - modifiable: true), - Emoji( - name: 'backhand index pointing up: medium skin tone', - char: '\u{1F446}\u{1F3FD}', - shortName: 'point_up_2_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'backhand', - 'finger', - 'hand', - 'index', - 'medium skin tone', - 'point', - 'up', - 'uc8', - 'diversity', - 'body', - 'hands', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers' - ], - modifiable: true), - Emoji( - name: 'backhand index pointing up: medium-dark skin tone', - char: '\u{1F446}\u{1F3FE}', - shortName: 'point_up_2_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'backhand', - 'finger', - 'hand', - 'index', - 'medium-dark skin tone', - 'point', - 'up', - 'uc8', - 'diversity', - 'body', - 'hands', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers' - ], - modifiable: true), - Emoji( - name: 'backhand index pointing up: dark skin tone', - char: '\u{1F446}\u{1F3FF}', - shortName: 'point_up_2_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'backhand', - 'dark skin tone', - 'finger', - 'hand', - 'index', - 'point', - 'up', - 'uc8', - 'diversity', - 'body', - 'hands', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers' - ], - modifiable: true), - Emoji( - name: 'backhand index pointing down', - char: '\u{1F447}', - shortName: 'point_down', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'backhand', - 'down', - 'finger', - 'hand', - 'index', - 'point', - 'uc6', - 'diversity', - 'body', - 'hands', - 'click', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers' - ]), - Emoji( - name: 'backhand index pointing down: light skin tone', - char: '\u{1F447}\u{1F3FB}', - shortName: 'point_down_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'backhand', - 'down', - 'finger', - 'hand', - 'index', - 'light skin tone', - 'point', - 'uc8', - 'diversity', - 'body', - 'hands', - 'click', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers' - ], - modifiable: true), - Emoji( - name: 'backhand index pointing down: medium-light skin tone', - char: '\u{1F447}\u{1F3FC}', - shortName: 'point_down_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'backhand', - 'down', - 'finger', - 'hand', - 'index', - 'medium-light skin tone', - 'point', - 'uc8', - 'diversity', - 'body', - 'hands', - 'click', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers' - ], - modifiable: true), - Emoji( - name: 'backhand index pointing down: medium skin tone', - char: '\u{1F447}\u{1F3FD}', - shortName: 'point_down_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'backhand', - 'down', - 'finger', - 'hand', - 'index', - 'medium skin tone', - 'point', - 'uc8', - 'diversity', - 'body', - 'hands', - 'click', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers' - ], - modifiable: true), - Emoji( - name: 'backhand index pointing down: medium-dark skin tone', - char: '\u{1F447}\u{1F3FE}', - shortName: 'point_down_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'backhand', - 'down', - 'finger', - 'hand', - 'index', - 'medium-dark skin tone', - 'point', - 'uc8', - 'diversity', - 'body', - 'hands', - 'click', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers' - ], - modifiable: true), - Emoji( - name: 'backhand index pointing down: dark skin tone', - char: '\u{1F447}\u{1F3FF}', - shortName: 'point_down_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'backhand', - 'dark skin tone', - 'down', - 'finger', - 'hand', - 'index', - 'point', - 'uc8', - 'diversity', - 'body', - 'hands', - 'click', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers' - ], - modifiable: true), - Emoji( - name: 'index pointing up', - char: '\u{261D}\u{FE0F}', - shortName: 'point_up', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'finger', - 'hand', - 'index', - 'point', - 'up', - 'uc1', - 'diversity', - 'body', - 'hands', - 'emojione', - 'porn', - 'important', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'emoji one' - ]), - Emoji( - name: 'index pointing up: light skin tone', - char: '\u{261D}\u{1F3FB}', - shortName: 'point_up_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'finger', - 'hand', - 'index', - 'light skin tone', - 'point', - 'up', - 'uc8', - 'diversity', - 'body', - 'hands', - 'emojione', - 'porn', - 'important', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'emoji one' - ], - modifiable: true), - Emoji( - name: 'index pointing up: medium-light skin tone', - char: '\u{261D}\u{1F3FC}', - shortName: 'point_up_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'finger', - 'hand', - 'index', - 'medium-light skin tone', - 'point', - 'up', - 'uc8', - 'diversity', - 'body', - 'hands', - 'emojione', - 'porn', - 'important', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'emoji one' - ], - modifiable: true), - Emoji( - name: 'index pointing up: medium skin tone', - char: '\u{261D}\u{1F3FD}', - shortName: 'point_up_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'finger', - 'hand', - 'index', - 'medium skin tone', - 'point', - 'up', - 'uc8', - 'diversity', - 'body', - 'hands', - 'emojione', - 'porn', - 'important', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'emoji one' - ], - modifiable: true), - Emoji( - name: 'index pointing up: medium-dark skin tone', - char: '\u{261D}\u{1F3FE}', - shortName: 'point_up_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'finger', - 'hand', - 'index', - 'medium-dark skin tone', - 'point', - 'up', - 'uc8', - 'diversity', - 'body', - 'hands', - 'emojione', - 'porn', - 'important', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'emoji one' - ], - modifiable: true), - Emoji( - name: 'index pointing up: dark skin tone', - char: '\u{261D}\u{1F3FF}', - shortName: 'point_up_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'dark skin tone', - 'finger', - 'hand', - 'index', - 'point', - 'up', - 'uc8', - 'diversity', - 'body', - 'hands', - 'emojione', - 'porn', - 'important', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'emoji one' - ], - modifiable: true), - Emoji( - name: 'raised hand', - char: '\u{270B}', - shortName: 'raised_hand', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'hand', - 'uc6', - 'diversity', - 'body', - 'hands', - 'hi', - 'girls night', - 'high five', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'ladies night', - 'girls only', - 'girlfriend' - ]), - Emoji( - name: 'raised hand: light skin tone', - char: '\u{270B}\u{1F3FB}', - shortName: 'raised_hand_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'hand', - 'light skin tone', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'girls night', - 'high five', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'ladies night', - 'girls only', - 'girlfriend' - ], - modifiable: true), - Emoji( - name: 'raised hand: medium-light skin tone', - char: '\u{270B}\u{1F3FC}', - shortName: 'raised_hand_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'hand', - 'medium-light skin tone', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'girls night', - 'high five', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'ladies night', - 'girls only', - 'girlfriend' - ], - modifiable: true), - Emoji( - name: 'raised hand: medium skin tone', - char: '\u{270B}\u{1F3FD}', - shortName: 'raised_hand_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'hand', - 'medium skin tone', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'girls night', - 'high five', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'ladies night', - 'girls only', - 'girlfriend' - ], - modifiable: true), - Emoji( - name: 'raised hand: medium-dark skin tone', - char: '\u{270B}\u{1F3FE}', - shortName: 'raised_hand_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'hand', - 'medium-dark skin tone', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'girls night', - 'high five', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'ladies night', - 'girls only', - 'girlfriend' - ], - modifiable: true), - Emoji( - name: 'raised hand: dark skin tone', - char: '\u{270B}\u{1F3FF}', - shortName: 'raised_hand_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'dark skin tone', - 'hand', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'girls night', - 'high five', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'ladies night', - 'girls only', - 'girlfriend' - ], - modifiable: true), - Emoji( - name: 'raised back of hand', - char: '\u{1F91A}', - shortName: 'raised_back_of_hand', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'backhand', - 'raised', - 'uc9', - 'diversity', - 'body', - 'hands', - 'award', - 'hi', - 'hate', - 'private', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'прив', - 'privé', - 'privado', - 'reserved' - ]), - Emoji( - name: 'raised back of hand: light skin tone', - char: '\u{1F91A}\u{1F3FB}', - shortName: 'raised_back_of_hand_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'backhand', - 'light skin tone', - 'raised', - 'uc9', - 'diversity', - 'body', - 'hands', - 'award', - 'hi', - 'hate', - 'private', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'raised back of hand: medium-light skin tone', - char: '\u{1F91A}\u{1F3FC}', - shortName: 'raised_back_of_hand_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'backhand', - 'medium-light skin tone', - 'raised', - 'uc9', - 'diversity', - 'body', - 'hands', - 'award', - 'hi', - 'hate', - 'private', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'raised back of hand: medium skin tone', - char: '\u{1F91A}\u{1F3FD}', - shortName: 'raised_back_of_hand_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'backhand', - 'medium skin tone', - 'raised', - 'uc9', - 'diversity', - 'body', - 'hands', - 'award', - 'hi', - 'hate', - 'private', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'raised back of hand: medium-dark skin tone', - char: '\u{1F91A}\u{1F3FE}', - shortName: 'raised_back_of_hand_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'backhand', - 'medium-dark skin tone', - 'raised', - 'uc9', - 'diversity', - 'body', - 'hands', - 'award', - 'hi', - 'hate', - 'private', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'raised back of hand: dark skin tone', - char: '\u{1F91A}\u{1F3FF}', - shortName: 'raised_back_of_hand_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'backhand', - 'dark skin tone', - 'raised', - 'uc9', - 'diversity', - 'body', - 'hands', - 'award', - 'hi', - 'hate', - 'private', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'hand with fingers splayed', - char: '\u{1F590}', - shortName: 'hand_splayed', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'finger', - 'hand', - 'splayed', - 'uc7', - 'diversity', - 'body', - 'hands', - 'hi', - 'gay pride', - 'high five', - 'proud', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle' - ]), - Emoji( - name: 'hand with fingers splayed: light skin tone', - char: '\u{1F590}\u{1F3FB}', - shortName: 'hand_splayed_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'finger', - 'hand', - 'light skin tone', - 'splayed', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'gay pride', - 'high five', - 'proud', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle' - ], - modifiable: true), - Emoji( - name: 'hand with fingers splayed: medium-light skin tone', - char: '\u{1F590}\u{1F3FC}', - shortName: 'hand_splayed_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'finger', - 'hand', - 'medium-light skin tone', - 'splayed', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'gay pride', - 'high five', - 'proud', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle' - ], - modifiable: true), - Emoji( - name: 'hand with fingers splayed: medium skin tone', - char: '\u{1F590}\u{1F3FD}', - shortName: 'hand_splayed_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'finger', - 'hand', - 'medium skin tone', - 'splayed', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'gay pride', - 'high five', - 'proud', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle' - ], - modifiable: true), - Emoji( - name: 'hand with fingers splayed: medium-dark skin tone', - char: '\u{1F590}\u{1F3FE}', - shortName: 'hand_splayed_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'finger', - 'hand', - 'medium-dark skin tone', - 'splayed', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'gay pride', - 'high five', - 'proud', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle' - ], - modifiable: true), - Emoji( - name: 'hand with fingers splayed: dark skin tone', - char: '\u{1F590}\u{1F3FF}', - shortName: 'hand_splayed_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'dark skin tone', - 'finger', - 'hand', - 'splayed', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'gay pride', - 'high five', - 'proud', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle' - ], - modifiable: true), - Emoji( - name: 'vulcan salute', - char: '\u{1F596}', - shortName: 'vulcan', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'finger', - 'hand', - 'spock', - 'vulcan', - 'uc7', - 'diversity', - 'body', - 'hands', - 'hi', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle' - ]), - Emoji( - name: 'vulcan salute: light skin tone', - char: '\u{1F596}\u{1F3FB}', - shortName: 'vulcan_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'finger', - 'hand', - 'light skin tone', - 'spock', - 'vulcan', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle' - ], - modifiable: true), - Emoji( - name: 'vulcan salute: medium-light skin tone', - char: '\u{1F596}\u{1F3FC}', - shortName: 'vulcan_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'finger', - 'hand', - 'medium-light skin tone', - 'spock', - 'vulcan', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle' - ], - modifiable: true), - Emoji( - name: 'vulcan salute: medium skin tone', - char: '\u{1F596}\u{1F3FD}', - shortName: 'vulcan_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'finger', - 'hand', - 'medium skin tone', - 'spock', - 'vulcan', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle' - ], - modifiable: true), - Emoji( - name: 'vulcan salute: medium-dark skin tone', - char: '\u{1F596}\u{1F3FE}', - shortName: 'vulcan_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'finger', - 'hand', - 'medium-dark skin tone', - 'spock', - 'vulcan', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle' - ], - modifiable: true), - Emoji( - name: 'vulcan salute: dark skin tone', - char: '\u{1F596}\u{1F3FF}', - shortName: 'vulcan_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'dark skin tone', - 'finger', - 'hand', - 'spock', - 'vulcan', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle' - ], - modifiable: true), - Emoji( - name: 'waving hand', - char: '\u{1F44B}', - shortName: 'wave', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'hand', - 'wave', - 'waving', - 'uc6', - 'diversity', - 'body', - 'hands', - 'hi', - 'hola', - 'friend', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo' - ]), - Emoji( - name: 'waving hand: light skin tone', - char: '\u{1F44B}\u{1F3FB}', - shortName: 'wave_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'hand', - 'light skin tone', - 'wave', - 'waving', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'hola', - 'friend', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo' - ], - modifiable: true), - Emoji( - name: 'waving hand: medium-light skin tone', - char: '\u{1F44B}\u{1F3FC}', - shortName: 'wave_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'hand', - 'medium-light skin tone', - 'wave', - 'waving', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'hola', - 'friend', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo' - ], - modifiable: true), - Emoji( - name: 'waving hand: medium skin tone', - char: '\u{1F44B}\u{1F3FD}', - shortName: 'wave_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'hand', - 'medium skin tone', - 'wave', - 'waving', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'hola', - 'friend', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo' - ], - modifiable: true), - Emoji( - name: 'waving hand: medium-dark skin tone', - char: '\u{1F44B}\u{1F3FE}', - shortName: 'wave_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'hand', - 'medium-dark skin tone', - 'wave', - 'waving', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'hola', - 'friend', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo' - ], - modifiable: true), - Emoji( - name: 'waving hand: dark skin tone', - char: '\u{1F44B}\u{1F3FF}', - shortName: 'wave_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersOpen, - keywords: [ - 'dark skin tone', - 'hand', - 'wave', - 'waving', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'hola', - 'friend', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo' - ], - modifiable: true), - Emoji( - name: 'call me hand', - char: '\u{1F919}', - shortName: 'call_me', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'call', - 'hand', - 'uc9', - 'diversity', - 'body', - 'hands', - 'hi', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle' - ]), - Emoji( - name: 'call me hand: light skin tone', - char: '\u{1F919}\u{1F3FB}', - shortName: 'call_me_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'call', - 'hand', - 'light skin tone', - 'uc9', - 'diversity', - 'body', - 'hands', - 'hi', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle' - ], - modifiable: true), - Emoji( - name: 'call me hand: medium-light skin tone', - char: '\u{1F919}\u{1F3FC}', - shortName: 'call_me_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'call', - 'hand', - 'medium-light skin tone', - 'uc9', - 'diversity', - 'body', - 'hands', - 'hi', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle' - ], - modifiable: true), - Emoji( - name: 'call me hand: medium skin tone', - char: '\u{1F919}\u{1F3FD}', - shortName: 'call_me_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'call', - 'hand', - 'medium skin tone', - 'uc9', - 'diversity', - 'body', - 'hands', - 'hi', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle' - ], - modifiable: true), - Emoji( - name: 'call me hand: medium-dark skin tone', - char: '\u{1F919}\u{1F3FE}', - shortName: 'call_me_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'call', - 'hand', - 'medium-dark skin tone', - 'uc9', - 'diversity', - 'body', - 'hands', - 'hi', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle' - ], - modifiable: true), - Emoji( - name: 'call me hand: dark skin tone', - char: '\u{1F919}\u{1F3FF}', - shortName: 'call_me_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handFingersPartial, - keywords: [ - 'call', - 'dark skin tone', - 'hand', - 'uc9', - 'diversity', - 'body', - 'hands', - 'hi', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle' - ], - modifiable: true), - Emoji( - name: 'flexed biceps', - char: '\u{1F4AA}', - shortName: 'muscle', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'biceps', - 'comic', - 'flex', - 'muscle', - 'uc6', - 'sport', - 'diversity', - 'body', - 'hands', - 'flex', - 'weight lifting', - 'win', - 'feminist', - 'boys night', - 'power', - 'handsome', - 'festivus', - 'protest', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'strong', - 'weight lifter', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'feminism', - 'strong woman', - 'guys night', - 'stud', - 'blm', - 'demonstration' - ]), - Emoji( - name: 'flexed biceps: light skin tone', - char: '\u{1F4AA}\u{1F3FB}', - shortName: 'muscle_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'biceps', - 'comic', - 'flex', - 'light skin tone', - 'muscle', - 'uc8', - 'sport', - 'diversity', - 'body', - 'hands', - 'flex', - 'weight lifting', - 'win', - 'feminist', - 'boys night', - 'power', - 'handsome', - 'festivus', - 'protest', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'strong', - 'weight lifter', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'feminism', - 'strong woman', - 'guys night', - 'stud', - 'blm', - 'demonstration' - ], - modifiable: true), - Emoji( - name: 'flexed biceps: medium-light skin tone', - char: '\u{1F4AA}\u{1F3FC}', - shortName: 'muscle_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'biceps', - 'comic', - 'flex', - 'medium-light skin tone', - 'muscle', - 'uc8', - 'sport', - 'diversity', - 'body', - 'hands', - 'flex', - 'weight lifting', - 'win', - 'feminist', - 'boys night', - 'power', - 'handsome', - 'festivus', - 'protest', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'strong', - 'weight lifter', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'feminism', - 'strong woman', - 'guys night', - 'stud', - 'blm', - 'demonstration' - ], - modifiable: true), - Emoji( - name: 'flexed biceps: medium skin tone', - char: '\u{1F4AA}\u{1F3FD}', - shortName: 'muscle_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'biceps', - 'comic', - 'flex', - 'medium skin tone', - 'muscle', - 'uc8', - 'sport', - 'diversity', - 'body', - 'hands', - 'flex', - 'weight lifting', - 'win', - 'feminist', - 'boys night', - 'power', - 'handsome', - 'festivus', - 'protest', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'strong', - 'weight lifter', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'feminism', - 'strong woman', - 'guys night', - 'stud', - 'blm', - 'demonstration' - ], - modifiable: true), - Emoji( - name: 'flexed biceps: medium-dark skin tone', - char: '\u{1F4AA}\u{1F3FE}', - shortName: 'muscle_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'biceps', - 'comic', - 'flex', - 'medium-dark skin tone', - 'muscle', - 'uc8', - 'sport', - 'diversity', - 'body', - 'hands', - 'flex', - 'weight lifting', - 'win', - 'feminist', - 'boys night', - 'power', - 'handsome', - 'festivus', - 'protest', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'strong', - 'weight lifter', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'feminism', - 'strong woman', - 'guys night', - 'stud', - 'blm', - 'demonstration' - ], - modifiable: true), - Emoji( - name: 'flexed biceps: dark skin tone', - char: '\u{1F4AA}\u{1F3FF}', - shortName: 'muscle_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'biceps', - 'comic', - 'dark skin tone', - 'flex', - 'muscle', - 'uc8', - 'sport', - 'diversity', - 'body', - 'hands', - 'flex', - 'weight lifting', - 'win', - 'feminist', - 'boys night', - 'power', - 'handsome', - 'festivus', - 'protest', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'strong', - 'weight lifter', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'feminism', - 'strong woman', - 'guys night', - 'stud', - 'blm', - 'demonstration' - ], - modifiable: true), - Emoji( - name: 'mechanical arm', - char: '\u{1F9BE}', - shortName: 'mechanical_arm', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'uc12', - 'body', - 'science', - 'handicap', - 'prosthetic', - 'fake arm', - 'accessibility', - 'body part', - 'anatomy', - 'lab', - 'disabled', - 'disability', - 'prosthetics', - 'robotic arm' - ]), - Emoji( - name: 'middle finger', - char: '\u{1F595}', - shortName: 'middle_finger', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'finger', - 'hand', - 'uc7', - 'diversity', - 'penis', - 'body', - 'hands', - 'angry', - 'middle finger', - 'sex', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'dick', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'flipping off', - 'fu', - 'the finger', - 'fuck you', - 'fuck off', - 'fuck', - 'fucking', - 'horny', - 'humping' - ]), - Emoji( - name: 'middle finger: light skin tone', - char: '\u{1F595}\u{1F3FB}', - shortName: 'middle_finger_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'finger', - 'hand', - 'light skin tone', - 'uc8', - 'diversity', - 'penis', - 'body', - 'hands', - 'angry', - 'middle finger', - 'sex', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'dick', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'flipping off', - 'fu', - 'the finger', - 'fuck you', - 'fuck off', - 'fuck', - 'fucking', - 'horny', - 'humping' - ], - modifiable: true), - Emoji( - name: 'middle finger: medium-light skin tone', - char: '\u{1F595}\u{1F3FC}', - shortName: 'middle_finger_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'finger', - 'hand', - 'medium-light skin tone', - 'uc8', - 'diversity', - 'penis', - 'body', - 'hands', - 'angry', - 'middle finger', - 'sex', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'dick', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'flipping off', - 'fu', - 'the finger', - 'fuck you', - 'fuck off', - 'fuck', - 'fucking', - 'horny', - 'humping' - ], - modifiable: true), - Emoji( - name: 'middle finger: medium skin tone', - char: '\u{1F595}\u{1F3FD}', - shortName: 'middle_finger_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'finger', - 'hand', - 'medium skin tone', - 'uc8', - 'diversity', - 'penis', - 'body', - 'hands', - 'angry', - 'middle finger', - 'sex', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'dick', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'flipping off', - 'fu', - 'the finger', - 'fuck you', - 'fuck off', - 'fuck', - 'fucking', - 'horny', - 'humping' - ], - modifiable: true), - Emoji( - name: 'middle finger: medium-dark skin tone', - char: '\u{1F595}\u{1F3FE}', - shortName: 'middle_finger_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'finger', - 'hand', - 'medium-dark skin tone', - 'uc8', - 'diversity', - 'penis', - 'body', - 'hands', - 'angry', - 'middle finger', - 'sex', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'dick', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'flipping off', - 'fu', - 'the finger', - 'fuck you', - 'fuck off', - 'fuck', - 'fucking', - 'horny', - 'humping' - ], - modifiable: true), - Emoji( - name: 'middle finger: dark skin tone', - char: '\u{1F595}\u{1F3FF}', - shortName: 'middle_finger_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handSingleFinger, - keywords: [ - 'dark skin tone', - 'finger', - 'hand', - 'uc8', - 'diversity', - 'penis', - 'body', - 'hands', - 'angry', - 'middle finger', - 'sex', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'dick', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'flipping off', - 'fu', - 'the finger', - 'fuck you', - 'fuck off', - 'fuck', - 'fucking', - 'horny', - 'humping' - ], - modifiable: true), - Emoji( - name: 'writing hand', - char: '\u{270D}\u{FE0F}', - shortName: 'writing_hand', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handProp, - keywords: [ - 'hand', - 'write', - 'uc1', - 'diversity', - 'body', - 'hands', - 'write', - 'color', - 'correct', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'writing', - 'colour', - 'coloring', - 'colouring', - 'drawing', - 'marker', - 'sketch', - 'passing grade' - ]), - Emoji( - name: 'writing hand: light skin tone', - char: '\u{270D}\u{1F3FB}', - shortName: 'writing_hand_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handProp, - keywords: [ - 'hand', - 'light skin tone', - 'write', - 'uc8', - 'diversity', - 'body', - 'hands', - 'write', - 'color', - 'correct', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'writing', - 'colour', - 'coloring', - 'colouring', - 'drawing', - 'marker', - 'sketch', - 'passing grade' - ], - modifiable: true), - Emoji( - name: 'writing hand: medium-light skin tone', - char: '\u{270D}\u{1F3FC}', - shortName: 'writing_hand_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handProp, - keywords: [ - 'hand', - 'medium-light skin tone', - 'write', - 'uc8', - 'diversity', - 'body', - 'hands', - 'write', - 'color', - 'correct', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'writing', - 'colour', - 'coloring', - 'colouring', - 'drawing', - 'marker', - 'sketch', - 'passing grade' - ], - modifiable: true), - Emoji( - name: 'writing hand: medium skin tone', - char: '\u{270D}\u{1F3FD}', - shortName: 'writing_hand_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handProp, - keywords: [ - 'hand', - 'medium skin tone', - 'write', - 'uc8', - 'diversity', - 'body', - 'hands', - 'write', - 'color', - 'correct', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'writing', - 'colour', - 'coloring', - 'colouring', - 'drawing', - 'marker', - 'sketch', - 'passing grade' - ], - modifiable: true), - Emoji( - name: 'writing hand: medium-dark skin tone', - char: '\u{270D}\u{1F3FE}', - shortName: 'writing_hand_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handProp, - keywords: [ - 'hand', - 'medium-dark skin tone', - 'write', - 'uc8', - 'diversity', - 'body', - 'hands', - 'write', - 'color', - 'correct', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'writing', - 'colour', - 'coloring', - 'colouring', - 'drawing', - 'marker', - 'sketch', - 'passing grade' - ], - modifiable: true), - Emoji( - name: 'writing hand: dark skin tone', - char: '\u{270D}\u{1F3FF}', - shortName: 'writing_hand_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handProp, - keywords: [ - 'dark skin tone', - 'hand', - 'write', - 'uc8', - 'diversity', - 'body', - 'hands', - 'write', - 'color', - 'correct', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'writing', - 'colour', - 'coloring', - 'colouring', - 'drawing', - 'marker', - 'sketch', - 'passing grade' - ], - modifiable: true), - Emoji( - name: 'folded hands', - char: '\u{1F64F}', - shortName: 'pray', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'ask', - 'bow', - 'folded', - 'gesture', - 'hand', - 'please', - 'pray', - 'thanks', - 'uc6', - 'diversity', - 'body', - 'hands', - 'hi', - 'luck', - 'thank you', - 'pray', - 'scientology', - 'jesus', - 'pleased', - 'yoga', - 'easter', - 'begging', - 'help', - 'hope', - 'soul', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'good luck', - 'lucky', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'scientologist', - 'please', - 'chill', - 'confident', - 'content', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'swear', - 'promise' - ]), - Emoji( - name: 'folded hands: light skin tone', - char: '\u{1F64F}\u{1F3FB}', - shortName: 'pray_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'ask', - 'bow', - 'folded', - 'gesture', - 'hand', - 'light skin tone', - 'please', - 'pray', - 'thanks', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'luck', - 'thank you', - 'pray', - 'scientology', - 'jesus', - 'pleased', - 'yoga', - 'easter', - 'begging', - 'help', - 'hope', - 'soul', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'good luck', - 'lucky', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'scientologist', - 'please', - 'chill', - 'confident', - 'content', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'swear', - 'promise' - ], - modifiable: true), - Emoji( - name: 'folded hands: medium-light skin tone', - char: '\u{1F64F}\u{1F3FC}', - shortName: 'pray_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'ask', - 'bow', - 'folded', - 'gesture', - 'hand', - 'medium-light skin tone', - 'please', - 'pray', - 'thanks', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'luck', - 'thank you', - 'pray', - 'scientology', - 'jesus', - 'pleased', - 'yoga', - 'easter', - 'begging', - 'help', - 'hope', - 'soul', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'good luck', - 'lucky', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'scientologist', - 'please', - 'chill', - 'confident', - 'content', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'swear', - 'promise' - ], - modifiable: true), - Emoji( - name: 'folded hands: medium skin tone', - char: '\u{1F64F}\u{1F3FD}', - shortName: 'pray_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'ask', - 'bow', - 'folded', - 'gesture', - 'hand', - 'medium skin tone', - 'please', - 'pray', - 'thanks', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'luck', - 'thank you', - 'pray', - 'scientology', - 'jesus', - 'pleased', - 'yoga', - 'easter', - 'begging', - 'help', - 'hope', - 'soul', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'good luck', - 'lucky', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'scientologist', - 'please', - 'chill', - 'confident', - 'content', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'swear', - 'promise' - ], - modifiable: true), - Emoji( - name: 'folded hands: medium-dark skin tone', - char: '\u{1F64F}\u{1F3FE}', - shortName: 'pray_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'ask', - 'bow', - 'folded', - 'gesture', - 'hand', - 'medium-dark skin tone', - 'please', - 'pray', - 'thanks', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'luck', - 'thank you', - 'pray', - 'scientology', - 'jesus', - 'pleased', - 'yoga', - 'easter', - 'begging', - 'help', - 'hope', - 'soul', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'good luck', - 'lucky', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'scientologist', - 'please', - 'chill', - 'confident', - 'content', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'swear', - 'promise' - ], - modifiable: true), - Emoji( - name: 'folded hands: dark skin tone', - char: '\u{1F64F}\u{1F3FF}', - shortName: 'pray_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.hands, - keywords: [ - 'ask', - 'bow', - 'dark skin tone', - 'folded', - 'gesture', - 'hand', - 'please', - 'pray', - 'thanks', - 'uc8', - 'diversity', - 'body', - 'hands', - 'hi', - 'luck', - 'thank you', - 'pray', - 'scientology', - 'jesus', - 'pleased', - 'yoga', - 'easter', - 'begging', - 'help', - 'hope', - 'soul', - 'language', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'good luck', - 'lucky', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'scientologist', - 'please', - 'chill', - 'confident', - 'content', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'swear', - 'promise' - ], - modifiable: true), - Emoji( - name: 'foot', - char: '\u{1F9B6}', - shortName: 'foot', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'uc11', - 'diversity', - 'body', - 'feet', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'toes', - 'heel', - 'ankle' - ]), - Emoji( - name: 'foot: light skin tone', - char: '\u{1F9B6}\u{1F3FB}', - shortName: 'foot_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'uc11', - 'diversity', - 'body', - 'feet', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'toes', - 'heel', - 'ankle' - ], - modifiable: true), - Emoji( - name: 'foot: medium-light skin tone', - char: '\u{1F9B6}\u{1F3FC}', - shortName: 'foot_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'uc11', - 'diversity', - 'body', - 'feet', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'toes', - 'heel', - 'ankle' - ], - modifiable: true), - Emoji( - name: 'foot: medium skin tone', - char: '\u{1F9B6}\u{1F3FD}', - shortName: 'foot_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'uc11', - 'diversity', - 'body', - 'feet', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'toes', - 'heel', - 'ankle' - ], - modifiable: true), - Emoji( - name: 'foot: medium-dark skin tone', - char: '\u{1F9B6}\u{1F3FE}', - shortName: 'foot_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'uc11', - 'diversity', - 'body', - 'feet', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'toes', - 'heel', - 'ankle' - ], - modifiable: true), - Emoji( - name: 'foot: dark skin tone', - char: '\u{1F9B6}\u{1F3FF}', - shortName: 'foot_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'uc11', - 'diversity', - 'body', - 'feet', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'toes', - 'heel', - 'ankle' - ], - modifiable: true), - Emoji( - name: 'leg', - char: '\u{1F9B5}', - shortName: 'leg', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'uc11', - 'diversity', - 'body', - 'feet', - 'knee', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'toes', - 'heel', - 'ankle', - 'thigh', - 'calf' - ]), - Emoji( - name: 'leg: light skin tone', - char: '\u{1F9B5}\u{1F3FB}', - shortName: 'leg_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'uc11', - 'diversity', - 'body', - 'feet', - 'knee', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'toes', - 'heel', - 'ankle', - 'thigh', - 'calf' - ], - modifiable: true), - Emoji( - name: 'leg: medium-light skin tone', - char: '\u{1F9B5}\u{1F3FC}', - shortName: 'leg_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'uc11', - 'diversity', - 'body', - 'feet', - 'knee', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'toes', - 'heel', - 'ankle', - 'thigh', - 'calf' - ], - modifiable: true), - Emoji( - name: 'leg: medium skin tone', - char: '\u{1F9B5}\u{1F3FD}', - shortName: 'leg_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'uc11', - 'diversity', - 'body', - 'feet', - 'knee', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'toes', - 'heel', - 'ankle', - 'thigh', - 'calf' - ], - modifiable: true), - Emoji( - name: 'leg: medium-dark skin tone', - char: '\u{1F9B5}\u{1F3FE}', - shortName: 'leg_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'uc11', - 'diversity', - 'body', - 'feet', - 'knee', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'toes', - 'heel', - 'ankle', - 'thigh', - 'calf' - ], - modifiable: true), - Emoji( - name: 'leg: dark skin tone', - char: '\u{1F9B5}\u{1F3FF}', - shortName: 'leg_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'uc11', - 'diversity', - 'body', - 'feet', - 'knee', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'toes', - 'heel', - 'ankle', - 'thigh', - 'calf' - ], - modifiable: true), - Emoji( - name: 'mechanical leg', - char: '\u{1F9BF}', - shortName: 'mechanical_leg', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'uc12', - 'body', - 'science', - 'handicap', - 'feet', - 'knee', - 'prosthetic', - 'fake leg', - 'accessibility', - 'medical', - 'body part', - 'anatomy', - 'lab', - 'disabled', - 'disability', - 'toes', - 'heel', - 'ankle', - 'thigh', - 'calf', - 'prosthetics', - 'robotic leg' - ]), - Emoji( - name: 'lipstick', - char: '\u{1F484}', - shortName: 'lipstick', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'cosmetics', - 'makeup', - 'uc6', - 'fashion', - 'women', - 'love', - 'sexy', - 'lipstick', - 'beautiful', - 'girls night', - 'color', - 'mirror', - 'clothes', - 'clothing', - 'style', - 'woman', - 'female', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'mouth', - 'mouths', - 'makeup', - 'lip', - 'lips', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'colour', - 'coloring', - 'colouring', - 'drawing', - 'marker', - 'sketch' - ]), - Emoji( - name: 'kiss mark', - char: '\u{1F48B}', - shortName: 'kiss', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.emotion, - keywords: [ - 'kiss', - 'lips', - 'uc6', - 'women', - 'love', - 'sexy', - 'lipstick', - 'beautiful', - 'girls night', - 'pink', - 'kisses', - 'mirror', - 'porn', - 'woman', - 'female', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'mouth', - 'mouths', - 'makeup', - 'lip', - 'lips', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'rose', - 'bisous', - 'beijos', - 'besos', - 'bise', - 'blowing kisses', - 'kissy' - ]), - Emoji( - name: 'mouth', - char: '\u{1F444}', - shortName: 'lips', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'lips', - 'uc6', - 'women', - 'body', - 'sexy', - 'lipstick', - 'beautiful', - 'porn', - 'woman', - 'female', - 'body part', - 'anatomy', - 'mouth', - 'mouths', - 'makeup', - 'lip', - 'lips', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely' - ]), - Emoji( - name: 'tooth', - char: '\u{1F9B7}', - shortName: 'tooth', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'uc11', - 'body', - 'teeth', - 'bite', - 'bones', - 'medical', - 'body part', - 'anatomy', - 'dentist', - 'Os', - 'hueso' - ]), - Emoji( - name: 'bone', - char: '\u{1F9B4}', - shortName: 'bone', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'uc11', - 'body', - 'science', - 'mystery', - 'bones', - 'medical', - 'body part', - 'anatomy', - 'lab', - 'Os', - 'hueso' - ]), - Emoji( - name: 'tongue', - char: '\u{1F445}', - shortName: 'tongue', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'body', - 'uc6', - 'body', - 'sexy', - 'sex', - 'lipstick', - 'pussy', - 'pink', - 'lick', - 'porn', - 'tongue', - 'medical', - 'body part', - 'anatomy', - 'fuck', - 'fucking', - 'horny', - 'humping', - 'mouth', - 'mouths', - 'makeup', - 'lip', - 'lips', - 'condom', - 'rose', - 'toung', - 'tounge' - ]), - Emoji( - name: 'ear', - char: '\u{1F442}', - shortName: 'ear', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'body', - 'uc6', - 'diversity', - 'body', - 'sound', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear' - ]), - Emoji( - name: 'ear: light skin tone', - char: '\u{1F442}\u{1F3FB}', - shortName: 'ear_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'body', - 'light skin tone', - 'uc8', - 'diversity', - 'body', - 'sound', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear' - ], - modifiable: true), - Emoji( - name: 'ear: medium-light skin tone', - char: '\u{1F442}\u{1F3FC}', - shortName: 'ear_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'body', - 'medium-light skin tone', - 'uc8', - 'diversity', - 'body', - 'sound', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear' - ], - modifiable: true), - Emoji( - name: 'ear: medium skin tone', - char: '\u{1F442}\u{1F3FD}', - shortName: 'ear_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'body', - 'medium skin tone', - 'uc8', - 'diversity', - 'body', - 'sound', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear' - ], - modifiable: true), - Emoji( - name: 'ear: medium-dark skin tone', - char: '\u{1F442}\u{1F3FE}', - shortName: 'ear_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'body', - 'medium-dark skin tone', - 'uc8', - 'diversity', - 'body', - 'sound', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear' - ], - modifiable: true), - Emoji( - name: 'ear: dark skin tone', - char: '\u{1F442}\u{1F3FF}', - shortName: 'ear_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'body', - 'dark skin tone', - 'uc8', - 'diversity', - 'body', - 'sound', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear' - ], - modifiable: true), - Emoji( - name: 'ear with hearing aid', - char: '\u{1F9BB}', - shortName: 'ear_with_hearing_aid', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'earphone', - 'sound', - 'deaf', - 'accessibility', - 'medical', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'earbud', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear', - 'hard of hearing' - ]), - Emoji( - name: 'ear with hearing aid: light skin tone', - char: '\u{1F9BB}\u{1F3FB}', - shortName: 'ear_with_hearing_aid_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'earphone', - 'sound', - 'deaf', - 'accessibility', - 'medical', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'earbud', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear', - 'hard of hearing' - ], - modifiable: true), - Emoji( - name: 'ear with hearing aid: medium-light skin tone', - char: '\u{1F9BB}\u{1F3FC}', - shortName: 'ear_with_hearing_aid_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'earphone', - 'sound', - 'deaf', - 'accessibility', - 'medical', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'earbud', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear', - 'hard of hearing' - ], - modifiable: true), - Emoji( - name: 'ear with hearing aid: medium skin tone', - char: '\u{1F9BB}\u{1F3FD}', - shortName: 'ear_with_hearing_aid_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'earphone', - 'sound', - 'deaf', - 'accessibility', - 'medical', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'earbud', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear', - 'hard of hearing' - ], - modifiable: true), - Emoji( - name: 'ear with hearing aid: medium-dark skin tone', - char: '\u{1F9BB}\u{1F3FE}', - shortName: 'ear_with_hearing_aid_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'earphone', - 'sound', - 'deaf', - 'accessibility', - 'medical', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'earbud', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear', - 'hard of hearing' - ], - modifiable: true), - Emoji( - name: 'ear with hearing aid: dark skin tone', - char: '\u{1F9BB}\u{1F3FF}', - shortName: 'ear_with_hearing_aid_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'earphone', - 'sound', - 'deaf', - 'accessibility', - 'medical', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'earbud', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear', - 'hard of hearing' - ], - modifiable: true), - Emoji( - name: 'nose', - char: '\u{1F443}', - shortName: 'nose', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'body', - 'uc6', - 'diversity', - 'body', - 'stinky', - 'booger', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'smell', - 'stink', - 'odor' - ]), - Emoji( - name: 'nose: light skin tone', - char: '\u{1F443}\u{1F3FB}', - shortName: 'nose_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'body', - 'light skin tone', - 'uc8', - 'diversity', - 'body', - 'stinky', - 'booger', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'smell', - 'stink', - 'odor' - ], - modifiable: true), - Emoji( - name: 'nose: medium-light skin tone', - char: '\u{1F443}\u{1F3FC}', - shortName: 'nose_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'body', - 'medium-light skin tone', - 'uc8', - 'diversity', - 'body', - 'stinky', - 'booger', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'smell', - 'stink', - 'odor' - ], - modifiable: true), - Emoji( - name: 'nose: medium skin tone', - char: '\u{1F443}\u{1F3FD}', - shortName: 'nose_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'body', - 'medium skin tone', - 'uc8', - 'diversity', - 'body', - 'stinky', - 'booger', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'smell', - 'stink', - 'odor' - ], - modifiable: true), - Emoji( - name: 'nose: medium-dark skin tone', - char: '\u{1F443}\u{1F3FE}', - shortName: 'nose_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'body', - 'medium-dark skin tone', - 'uc8', - 'diversity', - 'body', - 'stinky', - 'booger', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'smell', - 'stink', - 'odor' - ], - modifiable: true), - Emoji( - name: 'nose: dark skin tone', - char: '\u{1F443}\u{1F3FF}', - shortName: 'nose_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'body', - 'dark skin tone', - 'uc8', - 'diversity', - 'body', - 'stinky', - 'booger', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'smell', - 'stink', - 'odor' - ], - modifiable: true), - Emoji( - name: 'footprints', - char: '\u{1F463}', - shortName: 'footprints', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSymbol, - keywords: [ - 'clothing', - 'footprint', - 'print', - 'uc6', - 'baby', - 'paws', - 'feet', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'toes', - 'heel', - 'ankle' - ]), - Emoji( - name: 'eye', - char: '\u{1F441}\u{FE0F}', - shortName: 'eye', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'body', - 'uc7', - 'body', - 'eyes', - 'search', - 'medical', - 'body part', - 'anatomy', - 'eye', - 'eyebrow', - 'look', - 'find', - 'looking', - 'see' - ]), - Emoji( - name: 'eyes', - char: '\u{1F440}', - shortName: 'eyes', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'eye', - 'face', - 'uc6', - 'halloween', - 'body', - 'rolling eyes', - 'eyes', - 'google', - 'brain', - 'disney', - 'search', - 'eyeroll', - 'porn', - 'samhain', - 'body part', - 'anatomy', - 'eye roll', - 'side eye', - 'eye', - 'eyebrow', - 'mind', - 'memory', - 'thought', - 'conscience', - 'cartoon', - 'look', - 'find', - 'looking', - 'see' - ]), - Emoji( - name: 'brain', - char: '\u{1F9E0}', - shortName: 'brain', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'intelligent', - 'uc10', - 'halloween', - 'body', - 'science', - 'nerd', - 'brain', - 'medical', - 'samhain', - 'body part', - 'anatomy', - 'lab', - 'smart', - 'geek', - 'serious', - 'mind', - 'memory', - 'thought', - 'conscience' - ]), - Emoji( - name: 'anatomical heart', - char: '\u{1FAC0}', - shortName: 'anatomical_heart', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'uc13', - 'body', - 'science', - 'heart', - 'covid', - 'medical', - 'body part', - 'anatomy', - 'lab', - 'hearts', - 'serce', - 'corazón', - 'coração', - 'coeur' - ]), - Emoji( - name: 'lungs', - char: '\u{1FAC1}', - shortName: 'lungs', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.bodyParts, - keywords: [ - 'uc13', - 'body', - 'smoking', - 'science', - 'breathe', - 'covid', - 'medical', - 'body part', - 'anatomy', - 'smoke', - 'cigarette', - 'puff', - 'lab', - 'sigh', - 'inhale' - ]), - Emoji( - name: 'speaking head', - char: '\u{1F5E3}\u{FE0F}', - shortName: 'speaking_head', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSymbol, - keywords: [ - 'face', - 'head', - 'silhouette', - 'speak', - 'speaking', - 'uc7', - 'talk', - 'sound', - 'language', - 'talking', - 'speech', - 'social', - 'chat', - 'voice', - 'speechless', - 'speak', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear' - ]), - Emoji( - name: 'bust in silhouette', - char: '\u{1F464}', - shortName: 'bust_in_silhouette', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSymbol, - keywords: [ - 'bust', - 'silhouette', - 'uc6', - 'facebook', - 'fame', - 'famous', - 'celebrity' - ]), - Emoji( - name: 'busts in silhouette', - char: '\u{1F465}', - shortName: 'busts_in_silhouette', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSymbol, - keywords: ['bust', 'silhouette', 'uc6', 'magnet', 'facebook', 'network']), - Emoji( - name: 'people hugging', - char: '\u{1FAC2}', - shortName: 'people_hugging', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSymbol, - keywords: ['uc13', 'hug', 'embrace', 'hugs']), - Emoji( - name: 'baby', - char: '\u{1F476}', - shortName: 'baby', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'baby', - 'young', - 'uc6', - 'diversity', - 'baby', - 'christmas', - 'human', - 'child', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'gender', - 'people', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ]), - Emoji( - name: 'baby: light skin tone', - char: '\u{1F476}\u{1F3FB}', - shortName: 'baby_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'baby', - 'light skin tone', - 'young', - 'uc8', - 'diversity', - 'baby', - 'christmas', - 'human', - 'child', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'gender', - 'people', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'baby: medium-light skin tone', - char: '\u{1F476}\u{1F3FC}', - shortName: 'baby_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'baby', - 'medium-light skin tone', - 'young', - 'uc8', - 'diversity', - 'baby', - 'christmas', - 'human', - 'child', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'gender', - 'people', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'baby: medium skin tone', - char: '\u{1F476}\u{1F3FD}', - shortName: 'baby_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'baby', - 'medium skin tone', - 'young', - 'uc8', - 'diversity', - 'baby', - 'christmas', - 'human', - 'child', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'gender', - 'people', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'baby: medium-dark skin tone', - char: '\u{1F476}\u{1F3FE}', - shortName: 'baby_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'baby', - 'medium-dark skin tone', - 'young', - 'uc8', - 'diversity', - 'baby', - 'christmas', - 'human', - 'child', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'gender', - 'people', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'baby: dark skin tone', - char: '\u{1F476}\u{1F3FF}', - shortName: 'baby_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'baby', - 'dark skin tone', - 'young', - 'uc8', - 'diversity', - 'baby', - 'christmas', - 'human', - 'child', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'gender', - 'people', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'girl', - char: '\u{1F467}', - shortName: 'girl', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'Virgo', - 'young', - 'zodiac', - 'uc6', - 'diversity', - 'women', - 'beautiful', - 'human', - 'wife', - 'child', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ]), - Emoji( - name: 'girl: light skin tone', - char: '\u{1F467}\u{1F3FB}', - shortName: 'girl_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'Virgo', - 'light skin tone', - 'young', - 'zodiac', - 'uc8', - 'diversity', - 'women', - 'beautiful', - 'human', - 'wife', - 'child', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'girl: medium-light skin tone', - char: '\u{1F467}\u{1F3FC}', - shortName: 'girl_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'Virgo', - 'medium-light skin tone', - 'young', - 'zodiac', - 'uc8', - 'diversity', - 'women', - 'beautiful', - 'human', - 'wife', - 'child', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'girl: medium skin tone', - char: '\u{1F467}\u{1F3FD}', - shortName: 'girl_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'Virgo', - 'medium skin tone', - 'young', - 'zodiac', - 'uc8', - 'diversity', - 'women', - 'beautiful', - 'human', - 'wife', - 'child', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'girl: medium-dark skin tone', - char: '\u{1F467}\u{1F3FE}', - shortName: 'girl_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'Virgo', - 'medium-dark skin tone', - 'young', - 'zodiac', - 'uc8', - 'diversity', - 'women', - 'beautiful', - 'human', - 'wife', - 'child', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'girl: dark skin tone', - char: '\u{1F467}\u{1F3FF}', - shortName: 'girl_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'Virgo', - 'dark skin tone', - 'young', - 'zodiac', - 'uc8', - 'diversity', - 'women', - 'beautiful', - 'human', - 'wife', - 'child', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'child', - char: '\u{1F9D2}', - shortName: 'child', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc10', - 'men', - 'feminist', - 'beautiful', - 'human', - 'child', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'feminism', - 'strong woman', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ]), - Emoji( - name: 'child: light skin tone', - char: '\u{1F9D2}\u{1F3FB}', - shortName: 'child_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'gender-neutral', - 'light skin tone', - 'young', - 'uc10', - 'men', - 'feminist', - 'beautiful', - 'human', - 'child', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'feminism', - 'strong woman', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'child: medium-light skin tone', - char: '\u{1F9D2}\u{1F3FC}', - shortName: 'child_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'gender-neutral', - 'medium-light skin tone', - 'young', - 'uc10', - 'men', - 'feminist', - 'beautiful', - 'human', - 'child', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'feminism', - 'strong woman', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'child: medium skin tone', - char: '\u{1F9D2}\u{1F3FD}', - shortName: 'child_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'gender-neutral', - 'medium skin tone', - 'young', - 'uc10', - 'men', - 'feminist', - 'beautiful', - 'human', - 'child', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'feminism', - 'strong woman', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'child: medium-dark skin tone', - char: '\u{1F9D2}\u{1F3FE}', - shortName: 'child_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'gender-neutral', - 'medium-dark skin tone', - 'young', - 'uc10', - 'men', - 'feminist', - 'beautiful', - 'human', - 'child', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'feminism', - 'strong woman', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'child: dark skin tone', - char: '\u{1F9D2}\u{1F3FF}', - shortName: 'child_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'dark skin tone', - 'gender-neutral', - 'young', - 'uc10', - 'men', - 'feminist', - 'beautiful', - 'human', - 'child', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'feminism', - 'strong woman', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'boy', - char: '\u{1F466}', - shortName: 'boy', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'boy', - 'young', - 'uc6', - 'diversity', - 'men', - 'human', - 'handsome', - 'child', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'stud', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ]), - Emoji( - name: 'boy: light skin tone', - char: '\u{1F466}\u{1F3FB}', - shortName: 'boy_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'boy', - 'light skin tone', - 'young', - 'uc8', - 'diversity', - 'men', - 'human', - 'handsome', - 'child', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'stud', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'boy: medium-light skin tone', - char: '\u{1F466}\u{1F3FC}', - shortName: 'boy_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'boy', - 'medium-light skin tone', - 'young', - 'uc8', - 'diversity', - 'men', - 'human', - 'handsome', - 'child', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'stud', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'boy: medium skin tone', - char: '\u{1F466}\u{1F3FD}', - shortName: 'boy_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'boy', - 'medium skin tone', - 'young', - 'uc8', - 'diversity', - 'men', - 'human', - 'handsome', - 'child', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'stud', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'boy: medium-dark skin tone', - char: '\u{1F466}\u{1F3FE}', - shortName: 'boy_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'boy', - 'medium-dark skin tone', - 'young', - 'uc8', - 'diversity', - 'men', - 'human', - 'handsome', - 'child', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'stud', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'boy: dark skin tone', - char: '\u{1F466}\u{1F3FF}', - shortName: 'boy_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'boy', - 'dark skin tone', - 'young', - 'uc8', - 'diversity', - 'men', - 'human', - 'handsome', - 'child', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'stud', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'woman', - char: '\u{1F469}', - shortName: 'woman', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'woman', - 'uc6', - 'diversity', - 'lesbian', - 'women', - 'feminist', - 'beautiful', - 'girls night', - 'human', - 'parent', - 'wife', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'feminism', - 'strong woman', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'woman: light skin tone', - char: '\u{1F469}\u{1F3FB}', - shortName: 'woman_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'light skin tone', - 'woman', - 'uc8', - 'diversity', - 'lesbian', - 'women', - 'feminist', - 'beautiful', - 'girls night', - 'human', - 'parent', - 'wife', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'feminism', - 'strong woman', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman: medium-light skin tone', - char: '\u{1F469}\u{1F3FC}', - shortName: 'woman_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'medium-light skin tone', - 'woman', - 'uc8', - 'diversity', - 'lesbian', - 'women', - 'feminist', - 'beautiful', - 'girls night', - 'human', - 'parent', - 'wife', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'feminism', - 'strong woman', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman: medium skin tone', - char: '\u{1F469}\u{1F3FD}', - shortName: 'woman_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'medium skin tone', - 'woman', - 'uc8', - 'diversity', - 'lesbian', - 'women', - 'feminist', - 'beautiful', - 'girls night', - 'human', - 'parent', - 'wife', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'feminism', - 'strong woman', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman: medium-dark skin tone', - char: '\u{1F469}\u{1F3FE}', - shortName: 'woman_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'medium-dark skin tone', - 'woman', - 'uc8', - 'diversity', - 'lesbian', - 'women', - 'feminist', - 'beautiful', - 'girls night', - 'human', - 'parent', - 'wife', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'feminism', - 'strong woman', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman: dark skin tone', - char: '\u{1F469}\u{1F3FF}', - shortName: 'woman_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'dark skin tone', - 'woman', - 'uc8', - 'diversity', - 'lesbian', - 'women', - 'feminist', - 'beautiful', - 'girls night', - 'human', - 'parent', - 'wife', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'feminism', - 'strong woman', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person', - char: '\u{1F9D1}', - shortName: 'adult', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc10', - 'diversity', - 'lesbian', - 'men', - 'feminist', - 'girls night', - 'boys night', - 'human', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'feminism', - 'strong woman', - 'ladies night', - 'girls only', - 'girlfriend', - 'guys night', - 'gender', - 'people', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'person: light skin tone', - char: '\u{1F9D1}\u{1F3FB}', - shortName: 'adult_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'gender-neutral', - 'light skin tone', - 'uc10', - 'diversity', - 'lesbian', - 'men', - 'feminist', - 'girls night', - 'boys night', - 'human', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'feminism', - 'strong woman', - 'ladies night', - 'girls only', - 'girlfriend', - 'guys night', - 'gender', - 'people', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person: medium-light skin tone', - char: '\u{1F9D1}\u{1F3FC}', - shortName: 'adult_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'gender-neutral', - 'medium-light skin tone', - 'uc10', - 'diversity', - 'lesbian', - 'men', - 'feminist', - 'girls night', - 'boys night', - 'human', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'feminism', - 'strong woman', - 'ladies night', - 'girls only', - 'girlfriend', - 'guys night', - 'gender', - 'people', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person: medium skin tone', - char: '\u{1F9D1}\u{1F3FD}', - shortName: 'adult_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'gender-neutral', - 'medium skin tone', - 'uc10', - 'diversity', - 'lesbian', - 'men', - 'feminist', - 'girls night', - 'boys night', - 'human', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'feminism', - 'strong woman', - 'ladies night', - 'girls only', - 'girlfriend', - 'guys night', - 'gender', - 'people', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person: medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FE}', - shortName: 'adult_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'gender-neutral', - 'medium-dark skin tone', - 'uc10', - 'diversity', - 'lesbian', - 'men', - 'feminist', - 'girls night', - 'boys night', - 'human', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'feminism', - 'strong woman', - 'ladies night', - 'girls only', - 'girlfriend', - 'guys night', - 'gender', - 'people', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person: dark skin tone', - char: '\u{1F9D1}\u{1F3FF}', - shortName: 'adult_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'dark skin tone', - 'gender-neutral', - 'uc10', - 'diversity', - 'lesbian', - 'men', - 'feminist', - 'girls night', - 'boys night', - 'human', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'feminism', - 'strong woman', - 'ladies night', - 'girls only', - 'girlfriend', - 'guys night', - 'gender', - 'people', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'man', - char: '\u{1F468}', - shortName: 'man', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'man', - 'uc6', - 'diversity', - 'men', - 'boys night', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud' - ]), - Emoji( - name: 'man: light skin tone', - char: '\u{1F468}\u{1F3FB}', - shortName: 'man_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'light skin tone', - 'man', - 'uc8', - 'diversity', - 'men', - 'boys night', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man: medium-light skin tone', - char: '\u{1F468}\u{1F3FC}', - shortName: 'man_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'man', - 'medium-light skin tone', - 'uc8', - 'diversity', - 'men', - 'boys night', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man: medium skin tone', - char: '\u{1F468}\u{1F3FD}', - shortName: 'man_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'man', - 'medium skin tone', - 'uc8', - 'diversity', - 'men', - 'boys night', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man: medium-dark skin tone', - char: '\u{1F468}\u{1F3FE}', - shortName: 'man_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'man', - 'medium-dark skin tone', - 'uc8', - 'diversity', - 'men', - 'boys night', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man: dark skin tone', - char: '\u{1F468}\u{1F3FF}', - shortName: 'man_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'dark skin tone', - 'man', - 'uc8', - 'diversity', - 'men', - 'boys night', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud' - ], - modifiable: true), - Emoji( - name: 'person: curly hair', - char: '\u{1F9D1}\u{200D}\u{1F9B1}', - shortName: 'person_curly_hair', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc12', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'afro', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - "'fro", - 'curls', - 'frizzy', - 'perm' - ]), - Emoji( - name: 'person: light skin tone, curly hair', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F9B1}', - shortName: 'person_tone1_curly_hair', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc12', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'afro', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - "'fro", - 'curls', - 'frizzy', - 'perm' - ], - modifiable: true), - Emoji( - name: 'person: medium-light skin tone, curly hair', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F9B1}', - shortName: 'person_tone2_curly_hair', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc12', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'afro', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - "'fro", - 'curls', - 'frizzy', - 'perm' - ], - modifiable: true), - Emoji( - name: 'person: medium skin tone, curly hair', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F9B1}', - shortName: 'person_tone3_curly_hair', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc12', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'afro', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - "'fro", - 'curls', - 'frizzy', - 'perm' - ], - modifiable: true), - Emoji( - name: 'person: medium-dark skin tone, curly hair', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F9B1}', - shortName: 'person_tone4_curly_hair', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc12', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'afro', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - "'fro", - 'curls', - 'frizzy', - 'perm' - ], - modifiable: true), - Emoji( - name: 'person: dark skin tone, curly hair', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F9B1}', - shortName: 'person_tone5_curly_hair', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc12', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'afro', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - "'fro", - 'curls', - 'frizzy', - 'perm' - ], - modifiable: true), - Emoji( - name: 'woman: curly hair', - char: '\u{1F469}\u{200D}\u{1F9B1}', - shortName: 'woman_curly_haired', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'diversity', - 'lesbian', - 'women', - 'beautiful', - 'girls night', - 'human', - 'parent', - 'wife', - 'mom', - 'afro', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother', - "'fro", - 'curls', - 'frizzy', - 'perm' - ]), - Emoji( - name: 'woman: light skin tone, curly hair', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F9B1}', - shortName: 'woman_curly_haired_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'diversity', - 'lesbian', - 'women', - 'beautiful', - 'girls night', - 'human', - 'parent', - 'wife', - 'mom', - 'afro', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother', - "'fro", - 'curls', - 'frizzy', - 'perm' - ], - modifiable: true), - Emoji( - name: 'woman: medium-light skin tone, curly hair', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F9B1}', - shortName: 'woman_curly_haired_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'diversity', - 'lesbian', - 'women', - 'beautiful', - 'girls night', - 'human', - 'parent', - 'wife', - 'mom', - 'afro', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother', - "'fro", - 'curls', - 'frizzy', - 'perm' - ], - modifiable: true), - Emoji( - name: 'woman: medium skin tone, curly hair', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F9B1}', - shortName: 'woman_curly_haired_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'diversity', - 'lesbian', - 'women', - 'beautiful', - 'girls night', - 'human', - 'parent', - 'wife', - 'mom', - 'afro', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother', - "'fro", - 'curls', - 'frizzy', - 'perm' - ], - modifiable: true), - Emoji( - name: 'woman: medium-dark skin tone, curly hair', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F9B1}', - shortName: 'woman_curly_haired_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'diversity', - 'lesbian', - 'women', - 'beautiful', - 'girls night', - 'human', - 'parent', - 'wife', - 'mom', - 'afro', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother', - "'fro", - 'curls', - 'frizzy', - 'perm' - ], - modifiable: true), - Emoji( - name: 'woman: dark skin tone, curly hair', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F9B1}', - shortName: 'woman_curly_haired_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'diversity', - 'lesbian', - 'women', - 'beautiful', - 'girls night', - 'human', - 'parent', - 'wife', - 'mom', - 'afro', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother', - "'fro", - 'curls', - 'frizzy', - 'perm' - ], - modifiable: true), - Emoji( - name: 'man: curly hair', - char: '\u{1F468}\u{200D}\u{1F9B1}', - shortName: 'man_curly_haired', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'afro', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - "'fro", - 'curls', - 'frizzy', - 'perm' - ]), - Emoji( - name: 'man: light skin tone, curly hair', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{1F9B1}', - shortName: 'man_curly_haired_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'afro', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - "'fro", - 'curls', - 'frizzy', - 'perm' - ], - modifiable: true), - Emoji( - name: 'man: medium-light skin tone, curly hair', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{1F9B1}', - shortName: 'man_curly_haired_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'afro', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - "'fro", - 'curls', - 'frizzy', - 'perm' - ], - modifiable: true), - Emoji( - name: 'man: medium skin tone, curly hair', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{1F9B1}', - shortName: 'man_curly_haired_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'afro', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - "'fro", - 'curls', - 'frizzy', - 'perm' - ], - modifiable: true), - Emoji( - name: 'man: medium-dark skin tone, curly hair', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{1F9B1}', - shortName: 'man_curly_haired_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'afro', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - "'fro", - 'curls', - 'frizzy', - 'perm' - ], - modifiable: true), - Emoji( - name: 'man: dark skin tone, curly hair', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{1F9B1}', - shortName: 'man_curly_haired_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'afro', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - "'fro", - 'curls', - 'frizzy', - 'perm' - ], - modifiable: true), - Emoji( - name: 'person: red hair', - char: '\u{1F9D1}\u{200D}\u{1F9B0}', - shortName: 'person_red_hair', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc12', - 'lesbian', - 'men', - 'girls night', - 'human', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'ginger', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'person: light skin tone, red hair', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F9B0}', - shortName: 'person_tone1_red_hair', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc12', - 'lesbian', - 'men', - 'girls night', - 'human', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'ginger', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person: medium-light skin tone, red hair', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F9B0}', - shortName: 'person_tone2_red_hair', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc12', - 'lesbian', - 'men', - 'girls night', - 'human', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'ginger', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person: medium skin tone, red hair', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F9B0}', - shortName: 'person_tone3_red_hair', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc12', - 'lesbian', - 'men', - 'girls night', - 'human', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'ginger', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person: medium-dark skin tone, red hair', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F9B0}', - shortName: 'person_tone4_red_hair', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc12', - 'lesbian', - 'men', - 'girls night', - 'human', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'ginger', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person: dark skin tone, red hair', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F9B0}', - shortName: 'person_tone5_red_hair', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc12', - 'lesbian', - 'men', - 'girls night', - 'human', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'ginger', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman: red hair', - char: '\u{1F469}\u{200D}\u{1F9B0}', - shortName: 'woman_red_haired', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'diversity', - 'lesbian', - 'women', - 'beautiful', - 'human', - 'parent', - 'wife', - 'mom', - 'ginger', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'woman: light skin tone, red hair', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F9B0}', - shortName: 'woman_red_haired_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'diversity', - 'lesbian', - 'women', - 'beautiful', - 'human', - 'parent', - 'wife', - 'mom', - 'ginger', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman: medium-light skin tone, red hair', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F9B0}', - shortName: 'woman_red_haired_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'diversity', - 'lesbian', - 'women', - 'beautiful', - 'human', - 'parent', - 'wife', - 'mom', - 'ginger', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman: medium skin tone, red hair', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F9B0}', - shortName: 'woman_red_haired_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'diversity', - 'lesbian', - 'women', - 'beautiful', - 'human', - 'parent', - 'wife', - 'mom', - 'ginger', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman: medium-dark skin tone, red hair', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F9B0}', - shortName: 'woman_red_haired_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'diversity', - 'lesbian', - 'women', - 'beautiful', - 'human', - 'parent', - 'wife', - 'mom', - 'ginger', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman: dark skin tone, red hair', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F9B0}', - shortName: 'woman_red_haired_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'diversity', - 'lesbian', - 'women', - 'beautiful', - 'human', - 'parent', - 'wife', - 'mom', - 'ginger', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'man: red hair', - char: '\u{1F468}\u{200D}\u{1F9B0}', - shortName: 'man_red_haired', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'ginger', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud' - ]), - Emoji( - name: 'man: light skin tone, red hair', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{1F9B0}', - shortName: 'man_red_haired_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'ginger', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man: medium-light skin tone, red hair', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{1F9B0}', - shortName: 'man_red_haired_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'ginger', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man: medium skin tone, red hair', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{1F9B0}', - shortName: 'man_red_haired_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'ginger', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man: medium-dark skin tone, red hair', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{1F9B0}', - shortName: 'man_red_haired_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'ginger', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man: dark skin tone, red hair', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{1F9B0}', - shortName: 'man_red_haired_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'ginger', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud' - ], - modifiable: true), - Emoji( - name: 'woman: blond hair', - char: '\u{1F471}\u{200D}\u{2640}\u{FE0F}', - shortName: 'blond-haired_woman', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'blonde', - 'woman', - 'uc6', - 'diversity', - 'lesbian', - 'women', - 'feminist', - 'beautiful', - 'girls night', - 'human', - 'parent', - 'wife', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'feminism', - 'strong woman', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'woman: light skin tone, blond hair', - char: '\u{1F471}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'blond-haired_woman_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'blonde', - 'light skin tone', - 'woman', - 'uc8', - 'diversity', - 'lesbian', - 'women', - 'feminist', - 'beautiful', - 'girls night', - 'human', - 'parent', - 'wife', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'feminism', - 'strong woman', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman: medium-light skin tone, blond hair', - char: '\u{1F471}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'blond-haired_woman_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'blonde', - 'medium-light skin tone', - 'woman', - 'uc8', - 'diversity', - 'lesbian', - 'women', - 'feminist', - 'beautiful', - 'girls night', - 'human', - 'parent', - 'wife', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'feminism', - 'strong woman', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman: medium skin tone, blond hair', - char: '\u{1F471}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'blond-haired_woman_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'blonde', - 'medium skin tone', - 'woman', - 'uc8', - 'diversity', - 'lesbian', - 'women', - 'feminist', - 'beautiful', - 'girls night', - 'human', - 'parent', - 'wife', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'feminism', - 'strong woman', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman: medium-dark skin tone, blond hair', - char: '\u{1F471}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'blond-haired_woman_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'blonde', - 'medium-dark skin tone', - 'woman', - 'uc8', - 'diversity', - 'lesbian', - 'women', - 'feminist', - 'beautiful', - 'girls night', - 'human', - 'parent', - 'wife', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'feminism', - 'strong woman', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman: dark skin tone, blond hair', - char: '\u{1F471}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'blond-haired_woman_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'blonde', - 'dark skin tone', - 'woman', - 'uc8', - 'diversity', - 'lesbian', - 'women', - 'feminist', - 'beautiful', - 'girls night', - 'human', - 'parent', - 'wife', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'feminism', - 'strong woman', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person: blond hair', - char: '\u{1F471}', - shortName: 'blond_haired_person', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'blond', - 'uc6', - 'diversity', - 'lesbian', - 'men', - 'feminist', - 'boys night', - 'human', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'feminism', - 'strong woman', - 'guys night', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'person: light skin tone, blond hair', - char: '\u{1F471}\u{1F3FB}', - shortName: 'blond_haired_person_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'blond', - 'light skin tone', - 'uc8', - 'diversity', - 'lesbian', - 'men', - 'feminist', - 'boys night', - 'human', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'feminism', - 'strong woman', - 'guys night', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person: medium-light skin tone, blond hair', - char: '\u{1F471}\u{1F3FC}', - shortName: 'blond_haired_person_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'blond', - 'medium-light skin tone', - 'uc8', - 'diversity', - 'lesbian', - 'men', - 'feminist', - 'boys night', - 'human', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'feminism', - 'strong woman', - 'guys night', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person: medium skin tone, blond hair', - char: '\u{1F471}\u{1F3FD}', - shortName: 'blond_haired_person_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'blond', - 'medium skin tone', - 'uc8', - 'diversity', - 'lesbian', - 'men', - 'feminist', - 'boys night', - 'human', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'feminism', - 'strong woman', - 'guys night', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person: medium-dark skin tone, blond hair', - char: '\u{1F471}\u{1F3FE}', - shortName: 'blond_haired_person_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'blond', - 'medium-dark skin tone', - 'uc8', - 'diversity', - 'lesbian', - 'men', - 'feminist', - 'boys night', - 'human', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'feminism', - 'strong woman', - 'guys night', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person: dark skin tone, blond hair', - char: '\u{1F471}\u{1F3FF}', - shortName: 'blond_haired_person_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'blond', - 'dark skin tone', - 'uc8', - 'diversity', - 'lesbian', - 'men', - 'feminist', - 'boys night', - 'human', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'feminism', - 'strong woman', - 'guys night', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'man: blond hair', - char: '\u{1F471}\u{200D}\u{2642}\u{FE0F}', - shortName: 'blond-haired_man', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'blond', - 'man', - 'uc6', - 'diversity', - 'men', - 'boys night', - 'human', - 'daddy', - 'parent', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ]), - Emoji( - name: 'man: light skin tone, blond hair', - char: '\u{1F471}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'blond-haired_man_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'blond', - 'light skin tone', - 'man', - 'uc8', - 'diversity', - 'men', - 'boys night', - 'human', - 'daddy', - 'parent', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'man: medium-light skin tone, blond hair', - char: '\u{1F471}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'blond-haired_man_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'blond', - 'man', - 'medium-light skin tone', - 'uc8', - 'diversity', - 'men', - 'boys night', - 'human', - 'daddy', - 'parent', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'man: medium skin tone, blond hair', - char: '\u{1F471}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'blond-haired_man_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'blond', - 'man', - 'medium skin tone', - 'uc8', - 'diversity', - 'men', - 'boys night', - 'human', - 'daddy', - 'parent', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'man: medium-dark skin tone, blond hair', - char: '\u{1F471}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'blond-haired_man_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'blond', - 'man', - 'medium-dark skin tone', - 'uc8', - 'diversity', - 'men', - 'boys night', - 'human', - 'daddy', - 'parent', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'man: dark skin tone, blond hair', - char: '\u{1F471}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'blond-haired_man_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'blond', - 'dark skin tone', - 'man', - 'uc8', - 'diversity', - 'men', - 'boys night', - 'human', - 'daddy', - 'parent', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'person: white hair', - char: '\u{1F9D1}\u{200D}\u{1F9B3}', - shortName: 'person_white_hair', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc12', - 'old people', - 'lesbian', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother', - 'silver hair' - ]), - Emoji( - name: 'person: light skin tone, white hair', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F9B3}', - shortName: 'person_tone1_white_hair', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc12', - 'old people', - 'lesbian', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother', - 'silver hair' - ], - modifiable: true), - Emoji( - name: 'person: medium-light skin tone, white hair', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F9B3}', - shortName: 'person_tone2_white_hair', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc12', - 'old people', - 'lesbian', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother', - 'silver hair' - ], - modifiable: true), - Emoji( - name: 'person: medium skin tone, white hair', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F9B3}', - shortName: 'person_tone3_white_hair', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc12', - 'old people', - 'lesbian', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother', - 'silver hair' - ], - modifiable: true), - Emoji( - name: 'person: medium-dark skin tone, white hair', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F9B3}', - shortName: 'person_tone4_white_hair', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc12', - 'old people', - 'lesbian', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother', - 'silver hair' - ], - modifiable: true), - Emoji( - name: 'person: dark skin tone, white hair', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F9B3}', - shortName: 'person_tone5_white_hair', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc12', - 'old people', - 'lesbian', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother', - 'silver hair' - ], - modifiable: true), - Emoji( - name: 'woman: white hair', - char: '\u{1F469}\u{200D}\u{1F9B3}', - shortName: 'woman_white_haired', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'lesbian', - 'women', - 'beautiful', - 'human', - 'parent', - 'wife', - 'mom', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother', - 'silver hair' - ]), - Emoji( - name: 'woman: light skin tone, white hair', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F9B3}', - shortName: 'woman_white_haired_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'lesbian', - 'women', - 'beautiful', - 'human', - 'parent', - 'wife', - 'mom', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother', - 'silver hair' - ], - modifiable: true), - Emoji( - name: 'woman: medium-light skin tone, white hair', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F9B3}', - shortName: 'woman_white_haired_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'lesbian', - 'women', - 'beautiful', - 'human', - 'parent', - 'wife', - 'mom', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother', - 'silver hair' - ], - modifiable: true), - Emoji( - name: 'woman: medium skin tone, white hair', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F9B3}', - shortName: 'woman_white_haired_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'lesbian', - 'women', - 'beautiful', - 'human', - 'parent', - 'wife', - 'mom', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother', - 'silver hair' - ], - modifiable: true), - Emoji( - name: 'woman: medium-dark skin tone, white hair', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F9B3}', - shortName: 'woman_white_haired_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'lesbian', - 'women', - 'beautiful', - 'human', - 'parent', - 'wife', - 'mom', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother', - 'silver hair' - ], - modifiable: true), - Emoji( - name: 'woman: dark skin tone, white hair', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F9B3}', - shortName: 'woman_white_haired_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'lesbian', - 'women', - 'beautiful', - 'human', - 'parent', - 'wife', - 'mom', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother', - 'silver hair' - ], - modifiable: true), - Emoji( - name: 'man: white hair', - char: '\u{1F468}\u{200D}\u{1F9B3}', - shortName: 'man_white_haired', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'silver hair' - ]), - Emoji( - name: 'man: light skin tone, white hair', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{1F9B3}', - shortName: 'man_white_haired_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'silver hair' - ], - modifiable: true), - Emoji( - name: 'man: medium-light skin tone, white hair', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{1F9B3}', - shortName: 'man_white_haired_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'silver hair' - ], - modifiable: true), - Emoji( - name: 'man: medium skin tone, white hair', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{1F9B3}', - shortName: 'man_white_haired_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'silver hair' - ], - modifiable: true), - Emoji( - name: 'man: medium-dark skin tone, white hair', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{1F9B3}', - shortName: 'man_white_haired_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'silver hair' - ], - modifiable: true), - Emoji( - name: 'man: dark skin tone, white hair', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{1F9B3}', - shortName: 'man_white_haired_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'silver hair' - ], - modifiable: true), - Emoji( - name: 'person: bald', - char: '\u{1F9D1}\u{200D}\u{1F9B2}', - shortName: 'person_bald', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc12', - 'lesbian', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'shaved head', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'person: light skin tone, bald', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F9B2}', - shortName: 'person_tone1_bald', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc12', - 'lesbian', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'shaved head', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person: medium-light skin tone, bald', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F9B2}', - shortName: 'person_tone2_bald', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc12', - 'lesbian', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'shaved head', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person: medium skin tone, bald', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F9B2}', - shortName: 'person_tone3_bald', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc12', - 'lesbian', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'shaved head', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person: medium-dark skin tone, bald', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F9B2}', - shortName: 'person_tone4_bald', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc12', - 'lesbian', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'shaved head', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person: dark skin tone, bald', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F9B2}', - shortName: 'person_tone5_bald', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc12', - 'lesbian', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'shaved head', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman: bald', - char: '\u{1F469}\u{200D}\u{1F9B2}', - shortName: 'woman_bald', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'lesbian', - 'women', - 'beautiful', - 'human', - 'parent', - 'wife', - 'mom', - 'shaved head', - 'hairless', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother', - 'balding' - ]), - Emoji( - name: 'woman: light skin tone, bald', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F9B2}', - shortName: 'woman_bald_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'lesbian', - 'women', - 'beautiful', - 'human', - 'parent', - 'wife', - 'mom', - 'shaved head', - 'hairless', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother', - 'balding' - ], - modifiable: true), - Emoji( - name: 'woman: medium-light skin tone, bald', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F9B2}', - shortName: 'woman_bald_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'lesbian', - 'women', - 'beautiful', - 'human', - 'parent', - 'wife', - 'mom', - 'shaved head', - 'hairless', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother', - 'balding' - ], - modifiable: true), - Emoji( - name: 'woman: medium skin tone, bald', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F9B2}', - shortName: 'woman_bald_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'lesbian', - 'women', - 'beautiful', - 'human', - 'parent', - 'wife', - 'mom', - 'shaved head', - 'hairless', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother', - 'balding' - ], - modifiable: true), - Emoji( - name: 'woman: medium-dark skin tone, bald', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F9B2}', - shortName: 'woman_bald_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'lesbian', - 'women', - 'beautiful', - 'human', - 'parent', - 'wife', - 'mom', - 'shaved head', - 'hairless', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother', - 'balding' - ], - modifiable: true), - Emoji( - name: 'woman: dark skin tone, bald', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F9B2}', - shortName: 'woman_bald_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'lesbian', - 'women', - 'beautiful', - 'human', - 'parent', - 'wife', - 'mom', - 'shaved head', - 'hairless', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother', - 'balding' - ], - modifiable: true), - Emoji( - name: 'man: bald', - char: '\u{1F468}\u{200D}\u{1F9B2}', - shortName: 'man_bald', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'shaved head', - 'hairless', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'balding' - ]), - Emoji( - name: 'man: light skin tone, bald', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{1F9B2}', - shortName: 'man_bald_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'shaved head', - 'hairless', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'balding' - ], - modifiable: true), - Emoji( - name: 'man: medium-light skin tone, bald', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{1F9B2}', - shortName: 'man_bald_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'shaved head', - 'hairless', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'balding' - ], - modifiable: true), - Emoji( - name: 'man: medium skin tone, bald', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{1F9B2}', - shortName: 'man_bald_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'shaved head', - 'hairless', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'balding' - ], - modifiable: true), - Emoji( - name: 'man: medium-dark skin tone, bald', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{1F9B2}', - shortName: 'man_bald_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'shaved head', - 'hairless', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'balding' - ], - modifiable: true), - Emoji( - name: 'man: dark skin tone, bald', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{1F9B2}', - shortName: 'man_bald_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'shaved head', - 'hairless', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'balding' - ], - modifiable: true), - Emoji( - name: 'man: beard', - char: '\u{1F9D4}', - shortName: 'bearded_person', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc10', - 'diversity', - 'men', - 'boys night', - 'mustache', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'beard', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud' - ]), - Emoji( - name: 'man: light skin tone, beard', - char: '\u{1F9D4}\u{1F3FB}', - shortName: 'bearded_person_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'beard', - 'light skin tone', - 'uc10', - 'diversity', - 'men', - 'boys night', - 'mustache', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'beard', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man: medium-light skin tone, beard', - char: '\u{1F9D4}\u{1F3FC}', - shortName: 'bearded_person_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'beard', - 'medium-light skin tone', - 'uc10', - 'diversity', - 'men', - 'boys night', - 'mustache', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'beard', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man: medium skin tone, beard', - char: '\u{1F9D4}\u{1F3FD}', - shortName: 'bearded_person_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'beard', - 'medium skin tone', - 'uc10', - 'diversity', - 'men', - 'boys night', - 'mustache', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'beard', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man: medium-dark skin tone, beard', - char: '\u{1F9D4}\u{1F3FE}', - shortName: 'bearded_person_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'beard', - 'medium-dark skin tone', - 'uc10', - 'diversity', - 'men', - 'boys night', - 'mustache', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'beard', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man: dark skin tone, beard', - char: '\u{1F9D4}\u{1F3FF}', - shortName: 'bearded_person_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'beard', - 'dark skin tone', - 'uc10', - 'diversity', - 'men', - 'boys night', - 'mustache', - 'human', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'beard', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud' - ], - modifiable: true), - Emoji( - name: 'old woman', - char: '\u{1F475}', - shortName: 'older_woman', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'old', - 'woman', - 'uc6', - 'old people', - 'diversity', - 'lesbian', - 'women', - 'vintage', - 'human', - 'cane', - 'parent', - 'wife', - 'mom', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother', - 'silver hair' - ]), - Emoji( - name: 'old woman: light skin tone', - char: '\u{1F475}\u{1F3FB}', - shortName: 'older_woman_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'light skin tone', - 'old', - 'woman', - 'uc8', - 'old people', - 'diversity', - 'lesbian', - 'women', - 'vintage', - 'human', - 'cane', - 'parent', - 'wife', - 'mom', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother', - 'silver hair' - ], - modifiable: true), - Emoji( - name: 'old woman: medium-light skin tone', - char: '\u{1F475}\u{1F3FC}', - shortName: 'older_woman_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'medium-light skin tone', - 'old', - 'woman', - 'uc8', - 'old people', - 'diversity', - 'lesbian', - 'women', - 'vintage', - 'human', - 'cane', - 'parent', - 'wife', - 'mom', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother', - 'silver hair' - ], - modifiable: true), - Emoji( - name: 'old woman: medium skin tone', - char: '\u{1F475}\u{1F3FD}', - shortName: 'older_woman_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'medium skin tone', - 'old', - 'woman', - 'uc8', - 'old people', - 'diversity', - 'lesbian', - 'women', - 'vintage', - 'human', - 'cane', - 'parent', - 'wife', - 'mom', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother', - 'silver hair' - ], - modifiable: true), - Emoji( - name: 'old woman: medium-dark skin tone', - char: '\u{1F475}\u{1F3FE}', - shortName: 'older_woman_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'medium-dark skin tone', - 'old', - 'woman', - 'uc8', - 'old people', - 'diversity', - 'lesbian', - 'women', - 'vintage', - 'human', - 'cane', - 'parent', - 'wife', - 'mom', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother', - 'silver hair' - ], - modifiable: true), - Emoji( - name: 'old woman: dark skin tone', - char: '\u{1F475}\u{1F3FF}', - shortName: 'older_woman_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'dark skin tone', - 'old', - 'woman', - 'uc8', - 'old people', - 'diversity', - 'lesbian', - 'women', - 'vintage', - 'human', - 'cane', - 'parent', - 'wife', - 'mom', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother', - 'silver hair' - ], - modifiable: true), - Emoji( - name: 'older person', - char: '\u{1F9D3}', - shortName: 'older_adult', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'uc10', - 'old people', - 'diversity', - 'lesbian', - 'men', - 'feminist', - 'vintage', - 'human', - 'cane', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'feminism', - 'strong woman', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother', - 'silver hair' - ]), - Emoji( - name: 'older person: light skin tone', - char: '\u{1F9D3}\u{1F3FB}', - shortName: 'older_adult_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'gender-neutral', - 'light skin tone', - 'old', - 'uc10', - 'old people', - 'diversity', - 'lesbian', - 'men', - 'feminist', - 'vintage', - 'human', - 'cane', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'feminism', - 'strong woman', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother', - 'silver hair' - ], - modifiable: true), - Emoji( - name: 'older person: medium-light skin tone', - char: '\u{1F9D3}\u{1F3FC}', - shortName: 'older_adult_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'gender-neutral', - 'medium-light skin tone', - 'old', - 'uc10', - 'old people', - 'diversity', - 'lesbian', - 'men', - 'feminist', - 'vintage', - 'human', - 'cane', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'feminism', - 'strong woman', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother', - 'silver hair' - ], - modifiable: true), - Emoji( - name: 'older person: medium skin tone', - char: '\u{1F9D3}\u{1F3FD}', - shortName: 'older_adult_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'gender-neutral', - 'medium skin tone', - 'old', - 'uc10', - 'old people', - 'diversity', - 'lesbian', - 'men', - 'feminist', - 'vintage', - 'human', - 'cane', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'feminism', - 'strong woman', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother', - 'silver hair' - ], - modifiable: true), - Emoji( - name: 'older person: medium-dark skin tone', - char: '\u{1F9D3}\u{1F3FE}', - shortName: 'older_adult_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'gender-neutral', - 'medium-dark skin tone', - 'old', - 'uc10', - 'old people', - 'diversity', - 'lesbian', - 'men', - 'feminist', - 'vintage', - 'human', - 'cane', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'feminism', - 'strong woman', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother', - 'silver hair' - ], - modifiable: true), - Emoji( - name: 'older person: dark skin tone', - char: '\u{1F9D3}\u{1F3FF}', - shortName: 'older_adult_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'dark skin tone', - 'gender-neutral', - 'old', - 'uc10', - 'old people', - 'diversity', - 'lesbian', - 'men', - 'feminist', - 'vintage', - 'human', - 'cane', - 'daddy', - 'parent', - 'handsome', - 'wife', - 'husband', - 'mom', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'feminism', - 'strong woman', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'maman', - 'mommy', - 'mama', - 'mother', - 'silver hair' - ], - modifiable: true), - Emoji( - name: 'old man', - char: '\u{1F474}', - shortName: 'older_man', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'man', - 'old', - 'uc6', - 'old people', - 'diversity', - 'men', - 'vintage', - 'human', - 'cane', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'grey hair', - 'hairless', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'silver hair', - 'balding' - ]), - Emoji( - name: 'old man: light skin tone', - char: '\u{1F474}\u{1F3FB}', - shortName: 'older_man_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'light skin tone', - 'man', - 'old', - 'uc8', - 'old people', - 'diversity', - 'men', - 'vintage', - 'human', - 'cane', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'grey hair', - 'hairless', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'silver hair', - 'balding' - ], - modifiable: true), - Emoji( - name: 'old man: medium-light skin tone', - char: '\u{1F474}\u{1F3FC}', - shortName: 'older_man_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'man', - 'medium-light skin tone', - 'old', - 'uc8', - 'old people', - 'diversity', - 'men', - 'vintage', - 'human', - 'cane', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'grey hair', - 'hairless', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'silver hair', - 'balding' - ], - modifiable: true), - Emoji( - name: 'old man: medium skin tone', - char: '\u{1F474}\u{1F3FD}', - shortName: 'older_man_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'man', - 'medium skin tone', - 'old', - 'uc8', - 'old people', - 'diversity', - 'men', - 'vintage', - 'human', - 'cane', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'grey hair', - 'hairless', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'silver hair', - 'balding' - ], - modifiable: true), - Emoji( - name: 'old man: medium-dark skin tone', - char: '\u{1F474}\u{1F3FE}', - shortName: 'older_man_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'man', - 'medium-dark skin tone', - 'old', - 'uc8', - 'old people', - 'diversity', - 'men', - 'vintage', - 'human', - 'cane', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'grey hair', - 'hairless', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'silver hair', - 'balding' - ], - modifiable: true), - Emoji( - name: 'old man: dark skin tone', - char: '\u{1F474}\u{1F3FF}', - shortName: 'older_man_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.person, - keywords: [ - 'dark skin tone', - 'man', - 'old', - 'uc8', - 'old people', - 'diversity', - 'men', - 'vintage', - 'human', - 'cane', - 'daddy', - 'parent', - 'handsome', - 'husband', - 'grey hair', - 'hairless', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'stud', - 'silver hair', - 'balding' - ], - modifiable: true), - Emoji( - name: 'person with skullcap', - char: '\u{1F472}', - shortName: 'man_with_chinese_cap', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'gua pi mao', - 'hat', - 'man', - 'uc6', - 'diversity', - 'men', - 'human', - 'chinese', - 'parent', - 'handsome', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'chinois', - 'asian', - 'chine', - 'parents', - 'adult', - 'stud' - ]), - Emoji( - name: 'person with skullcap: light skin tone', - char: '\u{1F472}\u{1F3FB}', - shortName: 'man_with_chinese_cap_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'gua pi mao', - 'hat', - 'light skin tone', - 'man', - 'uc8', - 'diversity', - 'men', - 'human', - 'chinese', - 'parent', - 'handsome', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'chinois', - 'asian', - 'chine', - 'parents', - 'adult', - 'stud' - ], - modifiable: true), - Emoji( - name: 'person with skullcap: medium-light skin tone', - char: '\u{1F472}\u{1F3FC}', - shortName: 'man_with_chinese_cap_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'gua pi mao', - 'hat', - 'man', - 'medium-light skin tone', - 'uc8', - 'diversity', - 'men', - 'human', - 'chinese', - 'parent', - 'handsome', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'chinois', - 'asian', - 'chine', - 'parents', - 'adult', - 'stud' - ], - modifiable: true), - Emoji( - name: 'person with skullcap: medium skin tone', - char: '\u{1F472}\u{1F3FD}', - shortName: 'man_with_chinese_cap_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'gua pi mao', - 'hat', - 'man', - 'medium skin tone', - 'uc8', - 'diversity', - 'men', - 'human', - 'chinese', - 'parent', - 'handsome', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'chinois', - 'asian', - 'chine', - 'parents', - 'adult', - 'stud' - ], - modifiable: true), - Emoji( - name: 'person with skullcap: medium-dark skin tone', - char: '\u{1F472}\u{1F3FE}', - shortName: 'man_with_chinese_cap_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'gua pi mao', - 'hat', - 'man', - 'medium-dark skin tone', - 'uc8', - 'diversity', - 'men', - 'human', - 'chinese', - 'parent', - 'handsome', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'chinois', - 'asian', - 'chine', - 'parents', - 'adult', - 'stud' - ], - modifiable: true), - Emoji( - name: 'person with skullcap: dark skin tone', - char: '\u{1F472}\u{1F3FF}', - shortName: 'man_with_chinese_cap_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'gua pi mao', - 'hat', - 'man', - 'uc8', - 'diversity', - 'men', - 'human', - 'chinese', - 'parent', - 'handsome', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'chinois', - 'asian', - 'chine', - 'parents', - 'adult', - 'stud' - ], - modifiable: true), - Emoji( - name: 'person wearing turban', - char: '\u{1F473}', - shortName: 'person_wearing_turban', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'turban', - 'uc6', - 'diversity', - 'men', - 'human', - 'disney', - 'daddy', - 'islam', - 'parent', - 'wife', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'cartoon', - 'dad', - 'papa', - 'pere', - 'father', - 'muslim', - 'arab', - 'parents', - 'adult' - ]), - Emoji( - name: 'person wearing turban: light skin tone', - char: '\u{1F473}\u{1F3FB}', - shortName: 'person_wearing_turban_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'light skin tone', - 'turban', - 'uc8', - 'diversity', - 'men', - 'human', - 'disney', - 'daddy', - 'islam', - 'parent', - 'wife', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'cartoon', - 'dad', - 'papa', - 'pere', - 'father', - 'muslim', - 'arab', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'person wearing turban: medium-light skin tone', - char: '\u{1F473}\u{1F3FC}', - shortName: 'person_wearing_turban_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'medium-light skin tone', - 'turban', - 'uc8', - 'diversity', - 'men', - 'human', - 'disney', - 'daddy', - 'islam', - 'parent', - 'wife', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'cartoon', - 'dad', - 'papa', - 'pere', - 'father', - 'muslim', - 'arab', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'person wearing turban: medium skin tone', - char: '\u{1F473}\u{1F3FD}', - shortName: 'person_wearing_turban_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'medium skin tone', - 'turban', - 'uc8', - 'diversity', - 'men', - 'human', - 'disney', - 'daddy', - 'islam', - 'parent', - 'wife', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'cartoon', - 'dad', - 'papa', - 'pere', - 'father', - 'muslim', - 'arab', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'person wearing turban: medium-dark skin tone', - char: '\u{1F473}\u{1F3FE}', - shortName: 'person_wearing_turban_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'medium-dark skin tone', - 'turban', - 'uc8', - 'diversity', - 'men', - 'human', - 'disney', - 'daddy', - 'islam', - 'parent', - 'wife', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'cartoon', - 'dad', - 'papa', - 'pere', - 'father', - 'muslim', - 'arab', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'person wearing turban: dark skin tone', - char: '\u{1F473}\u{1F3FF}', - shortName: 'person_wearing_turban_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'turban', - 'uc8', - 'diversity', - 'men', - 'human', - 'disney', - 'daddy', - 'islam', - 'parent', - 'wife', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'cartoon', - 'dad', - 'papa', - 'pere', - 'father', - 'muslim', - 'arab', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'woman wearing turban', - char: '\u{1F473}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_wearing_turban', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'turban', - 'woman', - 'uc6', - 'diversity', - 'women', - 'human', - 'islam', - 'parent', - 'wife', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'gender', - 'people', - 'muslim', - 'arab', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'woman wearing turban: light skin tone', - char: '\u{1F473}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_wearing_turban_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'light skin tone', - 'turban', - 'woman', - 'uc8', - 'diversity', - 'women', - 'human', - 'islam', - 'parent', - 'wife', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'gender', - 'people', - 'muslim', - 'arab', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman wearing turban: medium-light skin tone', - char: '\u{1F473}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_wearing_turban_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'medium-light skin tone', - 'turban', - 'woman', - 'uc8', - 'diversity', - 'women', - 'human', - 'islam', - 'parent', - 'wife', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'gender', - 'people', - 'muslim', - 'arab', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman wearing turban: medium skin tone', - char: '\u{1F473}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_wearing_turban_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'medium skin tone', - 'turban', - 'woman', - 'uc8', - 'diversity', - 'women', - 'human', - 'islam', - 'parent', - 'wife', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'gender', - 'people', - 'muslim', - 'arab', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman wearing turban: medium-dark skin tone', - char: '\u{1F473}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_wearing_turban_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'medium-dark skin tone', - 'turban', - 'woman', - 'uc8', - 'diversity', - 'women', - 'human', - 'islam', - 'parent', - 'wife', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'gender', - 'people', - 'muslim', - 'arab', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman wearing turban: dark skin tone', - char: '\u{1F473}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_wearing_turban_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'turban', - 'woman', - 'uc8', - 'diversity', - 'women', - 'human', - 'islam', - 'parent', - 'wife', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'gender', - 'people', - 'muslim', - 'arab', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'man wearing turban', - char: '\u{1F473}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_wearing_turban', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'man', - 'turban', - 'uc6', - 'diversity', - 'men', - 'mustache', - 'human', - 'daddy', - 'islam', - 'parent', - 'handsome', - 'husband', - 'beard', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'muslim', - 'arab', - 'parents', - 'adult', - 'stud' - ]), - Emoji( - name: 'man wearing turban: light skin tone', - char: '\u{1F473}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_wearing_turban_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'light skin tone', - 'man', - 'turban', - 'uc8', - 'diversity', - 'men', - 'mustache', - 'human', - 'daddy', - 'islam', - 'parent', - 'handsome', - 'husband', - 'beard', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'muslim', - 'arab', - 'parents', - 'adult', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man wearing turban: medium-light skin tone', - char: '\u{1F473}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_wearing_turban_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'man', - 'medium-light skin tone', - 'turban', - 'uc8', - 'diversity', - 'men', - 'mustache', - 'human', - 'daddy', - 'islam', - 'parent', - 'handsome', - 'husband', - 'beard', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'muslim', - 'arab', - 'parents', - 'adult', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man wearing turban: medium skin tone', - char: '\u{1F473}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_wearing_turban_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'man', - 'medium skin tone', - 'turban', - 'uc8', - 'diversity', - 'men', - 'mustache', - 'human', - 'daddy', - 'islam', - 'parent', - 'handsome', - 'husband', - 'beard', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'muslim', - 'arab', - 'parents', - 'adult', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man wearing turban: medium-dark skin tone', - char: '\u{1F473}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_wearing_turban_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'man', - 'medium-dark skin tone', - 'turban', - 'uc8', - 'diversity', - 'men', - 'mustache', - 'human', - 'daddy', - 'islam', - 'parent', - 'handsome', - 'husband', - 'beard', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'muslim', - 'arab', - 'parents', - 'adult', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man wearing turban: dark skin tone', - char: '\u{1F473}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_wearing_turban_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'man', - 'turban', - 'uc8', - 'diversity', - 'men', - 'mustache', - 'human', - 'daddy', - 'islam', - 'parent', - 'handsome', - 'husband', - 'beard', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'muslim', - 'arab', - 'parents', - 'adult', - 'stud' - ], - modifiable: true), - Emoji( - name: 'woman with headscarf', - char: '\u{1F9D5}', - shortName: 'woman_with_headscarf', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc10', - 'diversity', - 'women', - 'girls night', - 'human', - 'parent', - 'hijab', - 'wife', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'woman with headscarf: light skin tone', - char: '\u{1F9D5}\u{1F3FB}', - shortName: 'woman_with_headscarf_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'headscarf', - 'hijab', - 'light skin tone', - 'mantilla', - 'tichel', - 'uc10', - 'diversity', - 'women', - 'girls night', - 'human', - 'parent', - 'hijab', - 'wife', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman with headscarf: medium-light skin tone', - char: '\u{1F9D5}\u{1F3FC}', - shortName: 'woman_with_headscarf_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'headscarf', - 'hijab', - 'mantilla', - 'medium-light skin tone', - 'tichel', - 'uc10', - 'diversity', - 'women', - 'girls night', - 'human', - 'parent', - 'hijab', - 'wife', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman with headscarf: medium skin tone', - char: '\u{1F9D5}\u{1F3FD}', - shortName: 'woman_with_headscarf_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'headscarf', - 'hijab', - 'mantilla', - 'medium skin tone', - 'tichel', - 'uc10', - 'diversity', - 'women', - 'girls night', - 'human', - 'parent', - 'hijab', - 'wife', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman with headscarf: medium-dark skin tone', - char: '\u{1F9D5}\u{1F3FE}', - shortName: 'woman_with_headscarf_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'headscarf', - 'hijab', - 'mantilla', - 'medium-dark skin tone', - 'tichel', - 'uc10', - 'diversity', - 'women', - 'girls night', - 'human', - 'parent', - 'hijab', - 'wife', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman with headscarf: dark skin tone', - char: '\u{1F9D5}\u{1F3FF}', - shortName: 'woman_with_headscarf_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'headscarf', - 'hijab', - 'mantilla', - 'tichel', - 'uc10', - 'diversity', - 'women', - 'girls night', - 'human', - 'parent', - 'hijab', - 'wife', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'ladies night', - 'girls only', - 'girlfriend', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'police officer', - char: '\u{1F46E}', - shortName: 'police_officer', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'cop', - 'officer', - 'police', - 'uc6', - 'diversity', - 'job', - 'police', - '911', - 'mustache', - 'power', - 'pig', - 'help', - 'private', - 'mystery', - 'court', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'cop', - 'policeman', - 'popo', - 'prison', - 'handcuff', - 'jail', - 'justice', - 'emergency', - 'injury', - 'pork', - 'прив', - 'privé', - 'privado', - 'reserved' - ]), - Emoji( - name: 'police officer: light skin tone', - char: '\u{1F46E}\u{1F3FB}', - shortName: 'police_officer_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'cop', - 'light skin tone', - 'officer', - 'police', - 'uc8', - 'diversity', - 'job', - 'police', - '911', - 'mustache', - 'power', - 'pig', - 'help', - 'private', - 'mystery', - 'court', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'cop', - 'policeman', - 'popo', - 'prison', - 'handcuff', - 'jail', - 'justice', - 'emergency', - 'injury', - 'pork', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'police officer: medium-light skin tone', - char: '\u{1F46E}\u{1F3FC}', - shortName: 'police_officer_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'cop', - 'medium-light skin tone', - 'officer', - 'police', - 'uc8', - 'diversity', - 'job', - 'police', - '911', - 'mustache', - 'power', - 'pig', - 'help', - 'private', - 'mystery', - 'court', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'cop', - 'policeman', - 'popo', - 'prison', - 'handcuff', - 'jail', - 'justice', - 'emergency', - 'injury', - 'pork', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'police officer: medium skin tone', - char: '\u{1F46E}\u{1F3FD}', - shortName: 'police_officer_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'cop', - 'medium skin tone', - 'officer', - 'police', - 'uc8', - 'diversity', - 'job', - 'police', - '911', - 'mustache', - 'power', - 'pig', - 'help', - 'private', - 'mystery', - 'court', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'cop', - 'policeman', - 'popo', - 'prison', - 'handcuff', - 'jail', - 'justice', - 'emergency', - 'injury', - 'pork', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'police officer: medium-dark skin tone', - char: '\u{1F46E}\u{1F3FE}', - shortName: 'police_officer_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'cop', - 'medium-dark skin tone', - 'officer', - 'police', - 'uc8', - 'diversity', - 'job', - 'police', - '911', - 'mustache', - 'power', - 'pig', - 'help', - 'private', - 'mystery', - 'court', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'cop', - 'policeman', - 'popo', - 'prison', - 'handcuff', - 'jail', - 'justice', - 'emergency', - 'injury', - 'pork', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'police officer: dark skin tone', - char: '\u{1F46E}\u{1F3FF}', - shortName: 'police_officer_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'cop', - 'dark skin tone', - 'officer', - 'police', - 'uc8', - 'diversity', - 'job', - 'police', - '911', - 'mustache', - 'power', - 'pig', - 'help', - 'private', - 'mystery', - 'court', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'cop', - 'policeman', - 'popo', - 'prison', - 'handcuff', - 'jail', - 'justice', - 'emergency', - 'injury', - 'pork', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'woman police officer', - char: '\u{1F46E}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_police_officer', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'cop', - 'officer', - 'police', - 'woman', - 'uc6', - 'diversity', - 'job', - 'police', - '911', - 'power', - 'pig', - 'help', - 'private', - 'mystery', - 'court', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'cop', - 'policeman', - 'popo', - 'prison', - 'handcuff', - 'jail', - 'justice', - 'emergency', - 'injury', - 'pork', - 'прив', - 'privé', - 'privado', - 'reserved' - ]), - Emoji( - name: 'woman police officer: light skin tone', - char: '\u{1F46E}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_police_officer_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'cop', - 'light skin tone', - 'officer', - 'police', - 'woman', - 'uc8', - 'diversity', - 'job', - 'police', - '911', - 'power', - 'pig', - 'help', - 'private', - 'mystery', - 'court', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'cop', - 'policeman', - 'popo', - 'prison', - 'handcuff', - 'jail', - 'justice', - 'emergency', - 'injury', - 'pork', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'woman police officer: medium-light skin tone', - char: '\u{1F46E}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_police_officer_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'cop', - 'medium-light skin tone', - 'officer', - 'police', - 'woman', - 'uc8', - 'diversity', - 'job', - 'police', - '911', - 'power', - 'pig', - 'help', - 'private', - 'mystery', - 'court', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'cop', - 'policeman', - 'popo', - 'prison', - 'handcuff', - 'jail', - 'justice', - 'emergency', - 'injury', - 'pork', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'woman police officer: medium skin tone', - char: '\u{1F46E}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_police_officer_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'cop', - 'medium skin tone', - 'officer', - 'police', - 'woman', - 'uc8', - 'diversity', - 'job', - 'police', - '911', - 'power', - 'pig', - 'help', - 'private', - 'mystery', - 'court', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'cop', - 'policeman', - 'popo', - 'prison', - 'handcuff', - 'jail', - 'justice', - 'emergency', - 'injury', - 'pork', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'woman police officer: medium-dark skin tone', - char: '\u{1F46E}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_police_officer_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'cop', - 'medium-dark skin tone', - 'officer', - 'police', - 'woman', - 'uc8', - 'diversity', - 'job', - 'police', - '911', - 'power', - 'pig', - 'help', - 'private', - 'mystery', - 'court', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'cop', - 'policeman', - 'popo', - 'prison', - 'handcuff', - 'jail', - 'justice', - 'emergency', - 'injury', - 'pork', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'woman police officer: dark skin tone', - char: '\u{1F46E}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_police_officer_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'cop', - 'dark skin tone', - 'officer', - 'police', - 'woman', - 'uc8', - 'diversity', - 'job', - 'police', - '911', - 'power', - 'pig', - 'help', - 'private', - 'mystery', - 'court', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'cop', - 'policeman', - 'popo', - 'prison', - 'handcuff', - 'jail', - 'justice', - 'emergency', - 'injury', - 'pork', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'man police officer', - char: '\u{1F46E}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_police_officer', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'cop', - 'man', - 'officer', - 'police', - 'uc6', - 'diversity', - 'job', - 'police', - '911', - 'mustache', - 'power', - 'pig', - 'help', - 'private', - 'mystery', - 'court', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'cop', - 'policeman', - 'popo', - 'prison', - 'handcuff', - 'jail', - 'justice', - 'emergency', - 'injury', - 'pork', - 'прив', - 'privé', - 'privado', - 'reserved' - ]), - Emoji( - name: 'man police officer: light skin tone', - char: '\u{1F46E}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_police_officer_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'cop', - 'light skin tone', - 'man', - 'officer', - 'police', - 'uc8', - 'diversity', - 'job', - 'police', - '911', - 'mustache', - 'power', - 'pig', - 'help', - 'private', - 'mystery', - 'court', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'cop', - 'policeman', - 'popo', - 'prison', - 'handcuff', - 'jail', - 'justice', - 'emergency', - 'injury', - 'pork', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'man police officer: medium-light skin tone', - char: '\u{1F46E}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_police_officer_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'cop', - 'man', - 'medium-light skin tone', - 'officer', - 'police', - 'uc8', - 'diversity', - 'job', - 'police', - '911', - 'mustache', - 'power', - 'pig', - 'help', - 'private', - 'mystery', - 'court', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'cop', - 'policeman', - 'popo', - 'prison', - 'handcuff', - 'jail', - 'justice', - 'emergency', - 'injury', - 'pork', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'man police officer: medium skin tone', - char: '\u{1F46E}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_police_officer_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'cop', - 'man', - 'medium skin tone', - 'officer', - 'police', - 'uc8', - 'diversity', - 'job', - 'police', - '911', - 'mustache', - 'power', - 'pig', - 'help', - 'private', - 'mystery', - 'court', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'cop', - 'policeman', - 'popo', - 'prison', - 'handcuff', - 'jail', - 'justice', - 'emergency', - 'injury', - 'pork', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'man police officer: medium-dark skin tone', - char: '\u{1F46E}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_police_officer_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'cop', - 'man', - 'medium-dark skin tone', - 'officer', - 'police', - 'uc8', - 'diversity', - 'job', - 'police', - '911', - 'mustache', - 'power', - 'pig', - 'help', - 'private', - 'mystery', - 'court', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'cop', - 'policeman', - 'popo', - 'prison', - 'handcuff', - 'jail', - 'justice', - 'emergency', - 'injury', - 'pork', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'man police officer: dark skin tone', - char: '\u{1F46E}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_police_officer_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'cop', - 'dark skin tone', - 'man', - 'officer', - 'police', - 'uc8', - 'diversity', - 'job', - 'police', - '911', - 'mustache', - 'power', - 'pig', - 'help', - 'private', - 'mystery', - 'court', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'cop', - 'policeman', - 'popo', - 'prison', - 'handcuff', - 'jail', - 'justice', - 'emergency', - 'injury', - 'pork', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'construction worker', - char: '\u{1F477}', - shortName: 'construction_worker', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'construction', - 'hat', - 'worker', - 'uc6', - 'diversity', - 'job', - 'build', - 'construction', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ]), - Emoji( - name: 'construction worker: light skin tone', - char: '\u{1F477}\u{1F3FB}', - shortName: 'construction_worker_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'construction', - 'hat', - 'light skin tone', - 'worker', - 'uc8', - 'diversity', - 'job', - 'build', - 'construction', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'construction worker: medium-light skin tone', - char: '\u{1F477}\u{1F3FC}', - shortName: 'construction_worker_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'construction', - 'hat', - 'medium-light skin tone', - 'worker', - 'uc8', - 'diversity', - 'job', - 'build', - 'construction', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'construction worker: medium skin tone', - char: '\u{1F477}\u{1F3FD}', - shortName: 'construction_worker_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'construction', - 'hat', - 'medium skin tone', - 'worker', - 'uc8', - 'diversity', - 'job', - 'build', - 'construction', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'construction worker: medium-dark skin tone', - char: '\u{1F477}\u{1F3FE}', - shortName: 'construction_worker_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'construction', - 'hat', - 'medium-dark skin tone', - 'worker', - 'uc8', - 'diversity', - 'job', - 'build', - 'construction', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'construction worker: dark skin tone', - char: '\u{1F477}\u{1F3FF}', - shortName: 'construction_worker_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'construction', - 'dark skin tone', - 'hat', - 'worker', - 'uc8', - 'diversity', - 'job', - 'build', - 'construction', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'woman construction worker', - char: '\u{1F477}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_construction_worker', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'construction', - 'woman', - 'worker', - 'uc6', - 'diversity', - 'job', - 'build', - 'construction', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ]), - Emoji( - name: 'woman construction worker: light skin tone', - char: '\u{1F477}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_construction_worker_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'construction', - 'light skin tone', - 'woman', - 'worker', - 'uc8', - 'diversity', - 'job', - 'build', - 'construction', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'woman construction worker: medium-light skin tone', - char: '\u{1F477}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_construction_worker_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'construction', - 'medium-light skin tone', - 'woman', - 'worker', - 'uc8', - 'diversity', - 'job', - 'build', - 'construction', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'woman construction worker: medium skin tone', - char: '\u{1F477}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_construction_worker_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'construction', - 'medium skin tone', - 'woman', - 'worker', - 'uc8', - 'diversity', - 'job', - 'build', - 'construction', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'woman construction worker: medium-dark skin tone', - char: '\u{1F477}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_construction_worker_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'construction', - 'medium-dark skin tone', - 'woman', - 'worker', - 'uc8', - 'diversity', - 'job', - 'build', - 'construction', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'woman construction worker: dark skin tone', - char: '\u{1F477}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_construction_worker_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'construction', - 'dark skin tone', - 'woman', - 'worker', - 'uc8', - 'diversity', - 'job', - 'build', - 'construction', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'man construction worker', - char: '\u{1F477}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_construction_worker', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'construction', - 'man', - 'worker', - 'uc6', - 'diversity', - 'job', - 'build', - 'construction', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ]), - Emoji( - name: 'man construction worker: light skin tone', - char: '\u{1F477}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_construction_worker_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'construction', - 'light skin tone', - 'man', - 'worker', - 'uc8', - 'diversity', - 'job', - 'build', - 'construction', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'man construction worker: medium-light skin tone', - char: '\u{1F477}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_construction_worker_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'construction', - 'man', - 'medium-light skin tone', - 'worker', - 'uc8', - 'diversity', - 'job', - 'build', - 'construction', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'man construction worker: medium skin tone', - char: '\u{1F477}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_construction_worker_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'construction', - 'man', - 'medium skin tone', - 'worker', - 'uc8', - 'diversity', - 'job', - 'build', - 'construction', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'man construction worker: medium-dark skin tone', - char: '\u{1F477}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_construction_worker_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'construction', - 'man', - 'medium-dark skin tone', - 'worker', - 'uc8', - 'diversity', - 'job', - 'build', - 'construction', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'man construction worker: dark skin tone', - char: '\u{1F477}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_construction_worker_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'construction', - 'dark skin tone', - 'man', - 'worker', - 'uc8', - 'diversity', - 'job', - 'build', - 'construction', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'guard', - char: '\u{1F482}', - shortName: 'guard', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'guard', - 'uc6', - 'diversity', - 'job', - 'queen', - 'england', - 'private', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'king', - 'prince', - 'princess', - 'united kingdom', - 'london', - 'uk', - 'прив', - 'privé', - 'privado', - 'reserved' - ]), - Emoji( - name: 'guard: light skin tone', - char: '\u{1F482}\u{1F3FB}', - shortName: 'guard_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'guard', - 'light skin tone', - 'uc8', - 'diversity', - 'job', - 'queen', - 'england', - 'private', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'king', - 'prince', - 'princess', - 'united kingdom', - 'london', - 'uk', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'guard: medium-light skin tone', - char: '\u{1F482}\u{1F3FC}', - shortName: 'guard_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'guard', - 'medium-light skin tone', - 'uc8', - 'diversity', - 'job', - 'queen', - 'england', - 'private', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'king', - 'prince', - 'princess', - 'united kingdom', - 'london', - 'uk', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'guard: medium skin tone', - char: '\u{1F482}\u{1F3FD}', - shortName: 'guard_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'guard', - 'medium skin tone', - 'uc8', - 'diversity', - 'job', - 'queen', - 'england', - 'private', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'king', - 'prince', - 'princess', - 'united kingdom', - 'london', - 'uk', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'guard: medium-dark skin tone', - char: '\u{1F482}\u{1F3FE}', - shortName: 'guard_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'guard', - 'medium-dark skin tone', - 'uc8', - 'diversity', - 'job', - 'queen', - 'england', - 'private', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'king', - 'prince', - 'princess', - 'united kingdom', - 'london', - 'uk', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'guard: dark skin tone', - char: '\u{1F482}\u{1F3FF}', - shortName: 'guard_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'guard', - 'uc8', - 'diversity', - 'job', - 'queen', - 'england', - 'private', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'king', - 'prince', - 'princess', - 'united kingdom', - 'london', - 'uk', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'woman guard', - char: '\u{1F482}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_guard', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'guard', - 'woman', - 'uc6', - 'diversity', - 'job', - 'queen', - 'england', - 'private', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'king', - 'prince', - 'princess', - 'united kingdom', - 'london', - 'uk', - 'прив', - 'privé', - 'privado', - 'reserved' - ]), - Emoji( - name: 'woman guard: light skin tone', - char: '\u{1F482}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_guard_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'guard', - 'light skin tone', - 'woman', - 'uc8', - 'diversity', - 'job', - 'queen', - 'england', - 'private', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'king', - 'prince', - 'princess', - 'united kingdom', - 'london', - 'uk', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'woman guard: medium-light skin tone', - char: '\u{1F482}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_guard_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'guard', - 'medium-light skin tone', - 'woman', - 'uc8', - 'diversity', - 'job', - 'queen', - 'england', - 'private', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'king', - 'prince', - 'princess', - 'united kingdom', - 'london', - 'uk', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'woman guard: medium skin tone', - char: '\u{1F482}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_guard_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'guard', - 'medium skin tone', - 'woman', - 'uc8', - 'diversity', - 'job', - 'queen', - 'england', - 'private', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'king', - 'prince', - 'princess', - 'united kingdom', - 'london', - 'uk', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'woman guard: medium-dark skin tone', - char: '\u{1F482}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_guard_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'guard', - 'medium-dark skin tone', - 'woman', - 'uc8', - 'diversity', - 'job', - 'queen', - 'england', - 'private', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'king', - 'prince', - 'princess', - 'united kingdom', - 'london', - 'uk', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'woman guard: dark skin tone', - char: '\u{1F482}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_guard_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'guard', - 'woman', - 'uc8', - 'diversity', - 'job', - 'queen', - 'england', - 'private', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'king', - 'prince', - 'princess', - 'united kingdom', - 'london', - 'uk', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'man guard', - char: '\u{1F482}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_guard', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'guard', - 'man', - 'uc6', - 'diversity', - 'job', - 'queen', - 'england', - 'private', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'king', - 'prince', - 'princess', - 'united kingdom', - 'london', - 'uk', - 'прив', - 'privé', - 'privado', - 'reserved' - ]), - Emoji( - name: 'man guard: light skin tone', - char: '\u{1F482}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_guard_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'guard', - 'light skin tone', - 'man', - 'uc8', - 'diversity', - 'job', - 'queen', - 'england', - 'private', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'king', - 'prince', - 'princess', - 'united kingdom', - 'london', - 'uk', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'man guard: medium-light skin tone', - char: '\u{1F482}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_guard_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'guard', - 'man', - 'medium-light skin tone', - 'uc8', - 'diversity', - 'job', - 'queen', - 'england', - 'private', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'king', - 'prince', - 'princess', - 'united kingdom', - 'london', - 'uk', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'man guard: medium skin tone', - char: '\u{1F482}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_guard_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'guard', - 'man', - 'medium skin tone', - 'uc8', - 'diversity', - 'job', - 'queen', - 'england', - 'private', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'king', - 'prince', - 'princess', - 'united kingdom', - 'london', - 'uk', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'man guard: medium-dark skin tone', - char: '\u{1F482}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_guard_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'guard', - 'man', - 'medium-dark skin tone', - 'uc8', - 'diversity', - 'job', - 'queen', - 'england', - 'private', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'king', - 'prince', - 'princess', - 'united kingdom', - 'london', - 'uk', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'man guard: dark skin tone', - char: '\u{1F482}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_guard_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'guard', - 'man', - 'uc8', - 'diversity', - 'job', - 'queen', - 'england', - 'private', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'king', - 'prince', - 'princess', - 'united kingdom', - 'london', - 'uk', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'detective', - char: '\u{1F575}', - shortName: 'detective', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'detective', - 'sleuth', - 'spy', - 'uc7', - 'diversity', - 'glasses', - 'halloween', - 'job', - 'google', - 'search', - 'detective', - 'super hero', - 'private', - 'mystery', - 'clever', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'eyeglasses', - 'eye glasses', - 'samhain', - 'profession', - 'boss', - 'career', - 'look', - 'find', - 'looking', - 'see', - 'superhero', - 'superman', - 'batman', - 'прив', - 'privé', - 'privado', - 'reserved', - 'witty' - ]), - Emoji( - name: 'detective: light skin tone', - char: '\u{1F575}\u{1F3FB}', - shortName: 'detective_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'detective', - 'light skin tone', - 'sleuth', - 'spy', - 'uc8', - 'diversity', - 'glasses', - 'halloween', - 'job', - 'google', - 'search', - 'detective', - 'super hero', - 'private', - 'mystery', - 'clever', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'eyeglasses', - 'eye glasses', - 'samhain', - 'profession', - 'boss', - 'career', - 'look', - 'find', - 'looking', - 'see', - 'superhero', - 'superman', - 'batman', - 'прив', - 'privé', - 'privado', - 'reserved', - 'witty' - ], - modifiable: true), - Emoji( - name: 'detective: medium-light skin tone', - char: '\u{1F575}\u{1F3FC}', - shortName: 'detective_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'detective', - 'medium-light skin tone', - 'sleuth', - 'spy', - 'uc8', - 'diversity', - 'glasses', - 'halloween', - 'job', - 'google', - 'search', - 'detective', - 'super hero', - 'private', - 'mystery', - 'clever', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'eyeglasses', - 'eye glasses', - 'samhain', - 'profession', - 'boss', - 'career', - 'look', - 'find', - 'looking', - 'see', - 'superhero', - 'superman', - 'batman', - 'прив', - 'privé', - 'privado', - 'reserved', - 'witty' - ], - modifiable: true), - Emoji( - name: 'detective: medium skin tone', - char: '\u{1F575}\u{1F3FD}', - shortName: 'detective_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'detective', - 'medium skin tone', - 'sleuth', - 'spy', - 'uc8', - 'diversity', - 'glasses', - 'halloween', - 'job', - 'google', - 'search', - 'detective', - 'super hero', - 'private', - 'mystery', - 'clever', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'eyeglasses', - 'eye glasses', - 'samhain', - 'profession', - 'boss', - 'career', - 'look', - 'find', - 'looking', - 'see', - 'superhero', - 'superman', - 'batman', - 'прив', - 'privé', - 'privado', - 'reserved', - 'witty' - ], - modifiable: true), - Emoji( - name: 'detective: medium-dark skin tone', - char: '\u{1F575}\u{1F3FE}', - shortName: 'detective_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'detective', - 'medium-dark skin tone', - 'sleuth', - 'spy', - 'uc8', - 'diversity', - 'glasses', - 'halloween', - 'job', - 'google', - 'search', - 'detective', - 'super hero', - 'private', - 'mystery', - 'clever', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'eyeglasses', - 'eye glasses', - 'samhain', - 'profession', - 'boss', - 'career', - 'look', - 'find', - 'looking', - 'see', - 'superhero', - 'superman', - 'batman', - 'прив', - 'privé', - 'privado', - 'reserved', - 'witty' - ], - modifiable: true), - Emoji( - name: 'detective: dark skin tone', - char: '\u{1F575}\u{1F3FF}', - shortName: 'detective_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'detective', - 'sleuth', - 'spy', - 'uc8', - 'diversity', - 'glasses', - 'halloween', - 'job', - 'google', - 'search', - 'detective', - 'super hero', - 'private', - 'mystery', - 'clever', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'eyeglasses', - 'eye glasses', - 'samhain', - 'profession', - 'boss', - 'career', - 'look', - 'find', - 'looking', - 'see', - 'superhero', - 'superman', - 'batman', - 'прив', - 'privé', - 'privado', - 'reserved', - 'witty' - ], - modifiable: true), - Emoji( - name: 'woman detective', - char: '\u{1F575}\u{FE0F}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_detective', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'detective', - 'sleuth', - 'spy', - 'woman', - 'uc7', - 'diversity', - 'halloween', - 'job', - 'search', - 'detective', - 'super hero', - 'private', - 'mystery', - 'clever', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'profession', - 'boss', - 'career', - 'look', - 'find', - 'looking', - 'see', - 'superhero', - 'superman', - 'batman', - 'прив', - 'privé', - 'privado', - 'reserved', - 'witty' - ]), - Emoji( - name: 'woman detective: light skin tone', - char: '\u{1F575}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_detective_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'detective', - 'light skin tone', - 'sleuth', - 'spy', - 'woman', - 'uc8', - 'diversity', - 'halloween', - 'job', - 'search', - 'detective', - 'super hero', - 'private', - 'mystery', - 'clever', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'profession', - 'boss', - 'career', - 'look', - 'find', - 'looking', - 'see', - 'superhero', - 'superman', - 'batman', - 'прив', - 'privé', - 'privado', - 'reserved', - 'witty' - ], - modifiable: true), - Emoji( - name: 'woman detective: medium-light skin tone', - char: '\u{1F575}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_detective_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'detective', - 'medium-light skin tone', - 'sleuth', - 'spy', - 'woman', - 'uc8', - 'diversity', - 'halloween', - 'job', - 'search', - 'detective', - 'super hero', - 'private', - 'mystery', - 'clever', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'profession', - 'boss', - 'career', - 'look', - 'find', - 'looking', - 'see', - 'superhero', - 'superman', - 'batman', - 'прив', - 'privé', - 'privado', - 'reserved', - 'witty' - ], - modifiable: true), - Emoji( - name: 'woman detective: medium skin tone', - char: '\u{1F575}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_detective_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'detective', - 'medium skin tone', - 'sleuth', - 'spy', - 'woman', - 'uc8', - 'diversity', - 'halloween', - 'job', - 'search', - 'detective', - 'super hero', - 'private', - 'mystery', - 'clever', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'profession', - 'boss', - 'career', - 'look', - 'find', - 'looking', - 'see', - 'superhero', - 'superman', - 'batman', - 'прив', - 'privé', - 'privado', - 'reserved', - 'witty' - ], - modifiable: true), - Emoji( - name: 'woman detective: medium-dark skin tone', - char: '\u{1F575}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_detective_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'detective', - 'medium-dark skin tone', - 'sleuth', - 'spy', - 'woman', - 'uc8', - 'diversity', - 'halloween', - 'job', - 'search', - 'detective', - 'super hero', - 'private', - 'mystery', - 'clever', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'profession', - 'boss', - 'career', - 'look', - 'find', - 'looking', - 'see', - 'superhero', - 'superman', - 'batman', - 'прив', - 'privé', - 'privado', - 'reserved', - 'witty' - ], - modifiable: true), - Emoji( - name: 'woman detective: dark skin tone', - char: '\u{1F575}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_detective_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'detective', - 'sleuth', - 'spy', - 'woman', - 'uc8', - 'diversity', - 'halloween', - 'job', - 'search', - 'detective', - 'super hero', - 'private', - 'mystery', - 'clever', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'profession', - 'boss', - 'career', - 'look', - 'find', - 'looking', - 'see', - 'superhero', - 'superman', - 'batman', - 'прив', - 'privé', - 'privado', - 'reserved', - 'witty' - ], - modifiable: true), - Emoji( - name: 'man detective', - char: '\u{1F575}\u{FE0F}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_detective', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'detective', - 'man', - 'sleuth', - 'spy', - 'uc7', - 'diversity', - 'halloween', - 'job', - 'search', - 'detective', - 'super hero', - 'private', - 'mystery', - 'clever', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'profession', - 'boss', - 'career', - 'look', - 'find', - 'looking', - 'see', - 'superhero', - 'superman', - 'batman', - 'прив', - 'privé', - 'privado', - 'reserved', - 'witty' - ]), - Emoji( - name: 'man detective: light skin tone', - char: '\u{1F575}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_detective_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'detective', - 'light skin tone', - 'man', - 'sleuth', - 'spy', - 'uc8', - 'diversity', - 'halloween', - 'job', - 'search', - 'detective', - 'super hero', - 'private', - 'mystery', - 'clever', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'profession', - 'boss', - 'career', - 'look', - 'find', - 'looking', - 'see', - 'superhero', - 'superman', - 'batman', - 'прив', - 'privé', - 'privado', - 'reserved', - 'witty' - ], - modifiable: true), - Emoji( - name: 'man detective: medium-light skin tone', - char: '\u{1F575}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_detective_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'detective', - 'man', - 'medium-light skin tone', - 'sleuth', - 'spy', - 'uc8', - 'diversity', - 'halloween', - 'job', - 'search', - 'detective', - 'super hero', - 'private', - 'mystery', - 'clever', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'profession', - 'boss', - 'career', - 'look', - 'find', - 'looking', - 'see', - 'superhero', - 'superman', - 'batman', - 'прив', - 'privé', - 'privado', - 'reserved', - 'witty' - ], - modifiable: true), - Emoji( - name: 'man detective: medium skin tone', - char: '\u{1F575}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_detective_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'detective', - 'man', - 'medium skin tone', - 'sleuth', - 'spy', - 'uc8', - 'diversity', - 'halloween', - 'job', - 'search', - 'detective', - 'super hero', - 'private', - 'mystery', - 'clever', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'profession', - 'boss', - 'career', - 'look', - 'find', - 'looking', - 'see', - 'superhero', - 'superman', - 'batman', - 'прив', - 'privé', - 'privado', - 'reserved', - 'witty' - ], - modifiable: true), - Emoji( - name: 'man detective: medium-dark skin tone', - char: '\u{1F575}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_detective_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'detective', - 'man', - 'medium-dark skin tone', - 'sleuth', - 'spy', - 'uc8', - 'diversity', - 'halloween', - 'job', - 'search', - 'detective', - 'super hero', - 'private', - 'mystery', - 'clever', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'profession', - 'boss', - 'career', - 'look', - 'find', - 'looking', - 'see', - 'superhero', - 'superman', - 'batman', - 'прив', - 'privé', - 'privado', - 'reserved', - 'witty' - ], - modifiable: true), - Emoji( - name: 'man detective: dark skin tone', - char: '\u{1F575}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_detective_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'detective', - 'man', - 'sleuth', - 'spy', - 'uc8', - 'diversity', - 'halloween', - 'job', - 'search', - 'detective', - 'super hero', - 'private', - 'mystery', - 'clever', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'profession', - 'boss', - 'career', - 'look', - 'find', - 'looking', - 'see', - 'superhero', - 'superman', - 'batman', - 'прив', - 'privé', - 'privado', - 'reserved', - 'witty' - ], - modifiable: true), - Emoji( - name: 'health worker', - char: '\u{1F9D1}\u{200D}\u{2695}\u{FE0F}', - shortName: 'health_worker', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'health', - 'sick', - 'job', - '911', - 'nerd', - 'nurse', - 'help', - 'disguise', - 'medical', - 'medicine', - 'doctor', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'smart', - 'geek', - 'serious' - ]), - Emoji( - name: 'health worker: light skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{2695}\u{FE0F}', - shortName: 'health_worker_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'health', - 'sick', - 'job', - '911', - 'nerd', - 'nurse', - 'help', - 'disguise', - 'medical', - 'medicine', - 'doctor', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'health worker: medium-light skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{2695}\u{FE0F}', - shortName: 'health_worker_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'health', - 'sick', - 'job', - '911', - 'nerd', - 'nurse', - 'help', - 'disguise', - 'medical', - 'medicine', - 'doctor', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'health worker: medium skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{2695}\u{FE0F}', - shortName: 'health_worker_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'health', - 'sick', - 'job', - '911', - 'nerd', - 'nurse', - 'help', - 'disguise', - 'medical', - 'medicine', - 'doctor', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'health worker: medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{2695}\u{FE0F}', - shortName: 'health_worker_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'health', - 'sick', - 'job', - '911', - 'nerd', - 'nurse', - 'help', - 'disguise', - 'medical', - 'medicine', - 'doctor', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'health worker: dark skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{2695}\u{FE0F}', - shortName: 'health_worker_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'health', - 'sick', - 'job', - '911', - 'nerd', - 'nurse', - 'help', - 'disguise', - 'medical', - 'medicine', - 'doctor', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'woman health worker', - char: '\u{1F469}\u{200D}\u{2695}\u{FE0F}', - shortName: 'woman_health_worker', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'doctor', - 'healthcare', - 'nurse', - 'therapist', - 'woman', - 'uc6', - 'diversity', - 'health', - 'sick', - 'job', - '911', - 'nerd', - 'nurse', - 'help', - 'disguise', - 'medical', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'medicine', - 'doctor', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'smart', - 'geek', - 'serious' - ]), - Emoji( - name: 'woman health worker: light skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{2695}\u{FE0F}', - shortName: 'woman_health_worker_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'doctor', - 'healthcare', - 'light skin tone', - 'nurse', - 'therapist', - 'woman', - 'uc8', - 'diversity', - 'health', - 'sick', - 'job', - '911', - 'nerd', - 'nurse', - 'help', - 'disguise', - 'medical', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'medicine', - 'doctor', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'woman health worker: medium-light skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{2695}\u{FE0F}', - shortName: 'woman_health_worker_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'doctor', - 'healthcare', - 'medium-light skin tone', - 'nurse', - 'therapist', - 'woman', - 'uc8', - 'diversity', - 'health', - 'sick', - 'job', - '911', - 'nerd', - 'nurse', - 'help', - 'disguise', - 'medical', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'medicine', - 'doctor', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'woman health worker: medium skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{2695}\u{FE0F}', - shortName: 'woman_health_worker_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'doctor', - 'healthcare', - 'medium skin tone', - 'nurse', - 'therapist', - 'woman', - 'uc8', - 'diversity', - 'health', - 'sick', - 'job', - '911', - 'nerd', - 'nurse', - 'help', - 'disguise', - 'medical', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'medicine', - 'doctor', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'woman health worker: medium-dark skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{2695}\u{FE0F}', - shortName: 'woman_health_worker_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'doctor', - 'healthcare', - 'medium-dark skin tone', - 'nurse', - 'therapist', - 'woman', - 'uc8', - 'diversity', - 'health', - 'sick', - 'job', - '911', - 'nerd', - 'nurse', - 'help', - 'disguise', - 'medical', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'medicine', - 'doctor', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'woman health worker: dark skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{2695}\u{FE0F}', - shortName: 'woman_health_worker_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'doctor', - 'healthcare', - 'nurse', - 'therapist', - 'woman', - 'uc8', - 'diversity', - 'health', - 'sick', - 'job', - '911', - 'nerd', - 'nurse', - 'help', - 'disguise', - 'medical', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'medicine', - 'doctor', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'man health worker', - char: '\u{1F468}\u{200D}\u{2695}\u{FE0F}', - shortName: 'man_health_worker', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'doctor', - 'healthcare', - 'man', - 'nurse', - 'therapist', - 'uc6', - 'diversity', - 'health', - 'sick', - 'job', - '911', - 'nerd', - 'nurse', - 'help', - 'disguise', - 'medical', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'medicine', - 'doctor', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'smart', - 'geek', - 'serious' - ]), - Emoji( - name: 'man health worker: light skin tone', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{2695}\u{FE0F}', - shortName: 'man_health_worker_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'doctor', - 'healthcare', - 'light skin tone', - 'man', - 'nurse', - 'therapist', - 'uc8', - 'diversity', - 'health', - 'sick', - 'job', - '911', - 'nerd', - 'nurse', - 'help', - 'disguise', - 'medical', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'medicine', - 'doctor', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'man health worker: medium-light skin tone', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{2695}\u{FE0F}', - shortName: 'man_health_worker_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'doctor', - 'healthcare', - 'man', - 'medium-light skin tone', - 'nurse', - 'therapist', - 'uc8', - 'diversity', - 'health', - 'sick', - 'job', - '911', - 'nerd', - 'nurse', - 'help', - 'disguise', - 'medical', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'medicine', - 'doctor', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'man health worker: medium skin tone', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{2695}\u{FE0F}', - shortName: 'man_health_worker_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'doctor', - 'healthcare', - 'man', - 'medium skin tone', - 'nurse', - 'therapist', - 'uc8', - 'diversity', - 'health', - 'sick', - 'job', - '911', - 'nerd', - 'nurse', - 'help', - 'disguise', - 'medical', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'medicine', - 'doctor', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'man health worker: medium-dark skin tone', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{2695}\u{FE0F}', - shortName: 'man_health_worker_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'doctor', - 'healthcare', - 'man', - 'medium-dark skin tone', - 'nurse', - 'therapist', - 'uc8', - 'diversity', - 'health', - 'sick', - 'job', - '911', - 'nerd', - 'nurse', - 'help', - 'disguise', - 'medical', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'medicine', - 'doctor', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'man health worker: dark skin tone', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{2695}\u{FE0F}', - shortName: 'man_health_worker_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'doctor', - 'healthcare', - 'man', - 'nurse', - 'therapist', - 'uc8', - 'diversity', - 'health', - 'sick', - 'job', - '911', - 'nerd', - 'nurse', - 'help', - 'disguise', - 'medical', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'medicine', - 'doctor', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'farmer', - char: '\u{1F9D1}\u{200D}\u{1F33E}', - shortName: 'farmer', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'farm', - 'disguise', - 'profession', - 'boss', - 'career' - ]), - Emoji( - name: 'farmer: light skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F33E}', - shortName: 'farmer_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'farm', - 'disguise', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'farmer: medium-light skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F33E}', - shortName: 'farmer_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'farm', - 'disguise', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'farmer: medium skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F33E}', - shortName: 'farmer_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'farm', - 'disguise', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'farmer: medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F33E}', - shortName: 'farmer_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'farm', - 'disguise', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'farmer: dark skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F33E}', - shortName: 'farmer_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'farm', - 'disguise', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'woman farmer', - char: '\u{1F469}\u{200D}\u{1F33E}', - shortName: 'woman_farmer', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'farmer', - 'gardener', - 'rancher', - 'woman', - 'uc6', - 'diversity', - 'job', - 'farm', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ]), - Emoji( - name: 'woman farmer: light skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F33E}', - shortName: 'woman_farmer_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'farmer', - 'gardener', - 'light skin tone', - 'rancher', - 'woman', - 'uc8', - 'diversity', - 'job', - 'farm', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'woman farmer: medium-light skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F33E}', - shortName: 'woman_farmer_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'farmer', - 'gardener', - 'medium-light skin tone', - 'rancher', - 'woman', - 'uc8', - 'diversity', - 'job', - 'farm', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'woman farmer: medium skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F33E}', - shortName: 'woman_farmer_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'farmer', - 'gardener', - 'medium skin tone', - 'rancher', - 'woman', - 'uc8', - 'diversity', - 'job', - 'farm', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'woman farmer: medium-dark skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F33E}', - shortName: 'woman_farmer_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'farmer', - 'gardener', - 'medium-dark skin tone', - 'rancher', - 'woman', - 'uc8', - 'diversity', - 'job', - 'farm', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'woman farmer: dark skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F33E}', - shortName: 'woman_farmer_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'farmer', - 'gardener', - 'rancher', - 'woman', - 'uc8', - 'diversity', - 'job', - 'farm', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'man farmer', - char: '\u{1F468}\u{200D}\u{1F33E}', - shortName: 'man_farmer', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'farmer', - 'gardener', - 'man', - 'rancher', - 'uc6', - 'diversity', - 'job', - 'farm', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ]), - Emoji( - name: 'man farmer: light skin tone', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{1F33E}', - shortName: 'man_farmer_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'farmer', - 'gardener', - 'light skin tone', - 'man', - 'rancher', - 'uc8', - 'diversity', - 'job', - 'farm', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'man farmer: medium-light skin tone', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{1F33E}', - shortName: 'man_farmer_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'farmer', - 'gardener', - 'man', - 'medium-light skin tone', - 'rancher', - 'uc8', - 'diversity', - 'job', - 'farm', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'man farmer: medium skin tone', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{1F33E}', - shortName: 'man_farmer_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'farmer', - 'gardener', - 'man', - 'medium skin tone', - 'rancher', - 'uc8', - 'diversity', - 'job', - 'farm', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'man farmer: medium-dark skin tone', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{1F33E}', - shortName: 'man_farmer_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'farmer', - 'gardener', - 'man', - 'medium-dark skin tone', - 'rancher', - 'uc8', - 'diversity', - 'job', - 'farm', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'man farmer: dark skin tone', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{1F33E}', - shortName: 'man_farmer_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'farmer', - 'gardener', - 'man', - 'rancher', - 'uc8', - 'diversity', - 'job', - 'farm', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'cook', - char: '\u{1F9D1}\u{200D}\u{1F373}', - shortName: 'cook', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'chef', - 'dinner', - 'disguise', - 'profession', - 'boss', - 'career', - 'cuisinière', - 'cuisinier', - 'lunch' - ]), - Emoji( - name: 'cook: light skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F373}', - shortName: 'cook_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'chef', - 'dinner', - 'disguise', - 'profession', - 'boss', - 'career', - 'cuisinière', - 'cuisinier', - 'lunch' - ], - modifiable: true), - Emoji( - name: 'cook: medium-light skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F373}', - shortName: 'cook_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'chef', - 'dinner', - 'disguise', - 'profession', - 'boss', - 'career', - 'cuisinière', - 'cuisinier', - 'lunch' - ], - modifiable: true), - Emoji( - name: 'cook: medium skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F373}', - shortName: 'cook_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'chef', - 'dinner', - 'disguise', - 'profession', - 'boss', - 'career', - 'cuisinière', - 'cuisinier', - 'lunch' - ], - modifiable: true), - Emoji( - name: 'cook: medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F373}', - shortName: 'cook_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'chef', - 'dinner', - 'disguise', - 'profession', - 'boss', - 'career', - 'cuisinière', - 'cuisinier', - 'lunch' - ], - modifiable: true), - Emoji( - name: 'cook: dark skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F373}', - shortName: 'cook_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'chef', - 'dinner', - 'disguise', - 'profession', - 'boss', - 'career', - 'cuisinière', - 'cuisinier', - 'lunch' - ], - modifiable: true), - Emoji( - name: 'woman cook', - char: '\u{1F469}\u{200D}\u{1F373}', - shortName: 'woman_cook', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'chef', - 'cook', - 'woman', - 'uc6', - 'diversity', - 'job', - 'bake', - 'chef', - 'dinner', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'baking', - 'cuisinière', - 'cuisinier', - 'lunch' - ]), - Emoji( - name: 'woman cook: light skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F373}', - shortName: 'woman_cook_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'chef', - 'cook', - 'light skin tone', - 'woman', - 'uc8', - 'diversity', - 'job', - 'bake', - 'chef', - 'dinner', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'baking', - 'cuisinière', - 'cuisinier', - 'lunch' - ], - modifiable: true), - Emoji( - name: 'woman cook: medium-light skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F373}', - shortName: 'woman_cook_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'chef', - 'cook', - 'medium-light skin tone', - 'woman', - 'uc8', - 'diversity', - 'job', - 'bake', - 'chef', - 'dinner', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'baking', - 'cuisinière', - 'cuisinier', - 'lunch' - ], - modifiable: true), - Emoji( - name: 'woman cook: medium skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F373}', - shortName: 'woman_cook_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'chef', - 'cook', - 'medium skin tone', - 'woman', - 'uc8', - 'diversity', - 'job', - 'bake', - 'chef', - 'dinner', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'baking', - 'cuisinière', - 'cuisinier', - 'lunch' - ], - modifiable: true), - Emoji( - name: 'woman cook: medium-dark skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F373}', - shortName: 'woman_cook_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'chef', - 'cook', - 'medium-dark skin tone', - 'woman', - 'uc8', - 'diversity', - 'job', - 'bake', - 'chef', - 'dinner', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'baking', - 'cuisinière', - 'cuisinier', - 'lunch' - ], - modifiable: true), - Emoji( - name: 'woman cook: dark skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F373}', - shortName: 'woman_cook_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'chef', - 'cook', - 'dark skin tone', - 'woman', - 'uc8', - 'diversity', - 'job', - 'bake', - 'chef', - 'dinner', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'baking', - 'cuisinière', - 'cuisinier', - 'lunch' - ], - modifiable: true), - Emoji( - name: 'man cook', - char: '\u{1F468}\u{200D}\u{1F373}', - shortName: 'man_cook', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'chef', - 'cook', - 'man', - 'uc6', - 'diversity', - 'job', - 'bake', - 'chef', - 'dinner', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'baking', - 'cuisinière', - 'cuisinier', - 'lunch' - ]), - Emoji( - name: 'man cook: light skin tone', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{1F373}', - shortName: 'man_cook_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'chef', - 'cook', - 'light skin tone', - 'man', - 'uc8', - 'diversity', - 'job', - 'bake', - 'chef', - 'dinner', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'baking', - 'cuisinière', - 'cuisinier', - 'lunch' - ], - modifiable: true), - Emoji( - name: 'man cook: medium-light skin tone', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{1F373}', - shortName: 'man_cook_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'chef', - 'cook', - 'man', - 'medium-light skin tone', - 'uc8', - 'diversity', - 'job', - 'bake', - 'chef', - 'dinner', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'baking', - 'cuisinière', - 'cuisinier', - 'lunch' - ], - modifiable: true), - Emoji( - name: 'man cook: medium skin tone', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{1F373}', - shortName: 'man_cook_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'chef', - 'cook', - 'man', - 'medium skin tone', - 'uc8', - 'diversity', - 'job', - 'bake', - 'chef', - 'dinner', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'baking', - 'cuisinière', - 'cuisinier', - 'lunch' - ], - modifiable: true), - Emoji( - name: 'man cook: medium-dark skin tone', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{1F373}', - shortName: 'man_cook_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'chef', - 'cook', - 'man', - 'medium-dark skin tone', - 'uc8', - 'diversity', - 'job', - 'bake', - 'chef', - 'dinner', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'baking', - 'cuisinière', - 'cuisinier', - 'lunch' - ], - modifiable: true), - Emoji( - name: 'man cook: dark skin tone', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{1F373}', - shortName: 'man_cook_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'chef', - 'cook', - 'dark skin tone', - 'man', - 'uc8', - 'diversity', - 'job', - 'bake', - 'chef', - 'dinner', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'baking', - 'cuisinière', - 'cuisinier', - 'lunch' - ], - modifiable: true), - Emoji( - name: 'student', - char: '\u{1F9D1}\u{200D}\u{1F393}', - shortName: 'student', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'classroom', - 'nerd', - 'graduate', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'smart', - 'geek', - 'serious' - ]), - Emoji( - name: 'student: light skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F393}', - shortName: 'student_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'classroom', - 'nerd', - 'graduate', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'student: medium-light skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F393}', - shortName: 'student_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'classroom', - 'nerd', - 'graduate', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'student: medium skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F393}', - shortName: 'student_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'classroom', - 'nerd', - 'graduate', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'student: medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F393}', - shortName: 'student_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'classroom', - 'nerd', - 'graduate', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'student: dark skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F393}', - shortName: 'student_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'classroom', - 'nerd', - 'graduate', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'woman student', - char: '\u{1F469}\u{200D}\u{1F393}', - shortName: 'woman_student', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'graduate', - 'student', - 'woman', - 'uc6', - 'diversity', - 'classroom', - 'nerd', - 'graduate', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'smart', - 'geek', - 'serious' - ]), - Emoji( - name: 'woman student: light skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F393}', - shortName: 'woman_student_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'graduate', - 'light skin tone', - 'student', - 'woman', - 'uc8', - 'diversity', - 'classroom', - 'nerd', - 'graduate', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'woman student: medium-light skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F393}', - shortName: 'woman_student_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'graduate', - 'medium-light skin tone', - 'student', - 'woman', - 'uc8', - 'diversity', - 'classroom', - 'nerd', - 'graduate', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'woman student: medium skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F393}', - shortName: 'woman_student_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'graduate', - 'medium skin tone', - 'student', - 'woman', - 'uc8', - 'diversity', - 'classroom', - 'nerd', - 'graduate', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'woman student: medium-dark skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F393}', - shortName: 'woman_student_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'graduate', - 'medium-dark skin tone', - 'student', - 'woman', - 'uc8', - 'diversity', - 'classroom', - 'nerd', - 'graduate', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'woman student: dark skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F393}', - shortName: 'woman_student_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'graduate', - 'student', - 'woman', - 'uc8', - 'diversity', - 'classroom', - 'nerd', - 'graduate', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'man student', - char: '\u{1F468}\u{200D}\u{1F393}', - shortName: 'man_student', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'graduate', - 'man', - 'student', - 'uc6', - 'diversity', - 'classroom', - 'nerd', - 'graduate', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'smart', - 'geek', - 'serious' - ]), - Emoji( - name: 'man student: light skin tone', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{1F393}', - shortName: 'man_student_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'graduate', - 'light skin tone', - 'man', - 'student', - 'uc8', - 'diversity', - 'classroom', - 'nerd', - 'graduate', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'man student: medium-light skin tone', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{1F393}', - shortName: 'man_student_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'graduate', - 'man', - 'medium-light skin tone', - 'student', - 'uc8', - 'diversity', - 'classroom', - 'nerd', - 'graduate', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'man student: medium skin tone', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{1F393}', - shortName: 'man_student_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'graduate', - 'man', - 'medium skin tone', - 'student', - 'uc8', - 'diversity', - 'classroom', - 'nerd', - 'graduate', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'man student: medium-dark skin tone', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{1F393}', - shortName: 'man_student_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'graduate', - 'man', - 'medium-dark skin tone', - 'student', - 'uc8', - 'diversity', - 'classroom', - 'nerd', - 'graduate', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'man student: dark skin tone', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{1F393}', - shortName: 'man_student_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'graduate', - 'man', - 'student', - 'uc8', - 'diversity', - 'classroom', - 'nerd', - 'graduate', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'singer', - char: '\u{1F9D1}\u{200D}\u{1F3A4}', - shortName: 'singer', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'rock and roll', - 'fame', - 'artist', - 'disguise', - 'profession', - 'boss', - 'career', - 'famous', - 'celebrity' - ]), - Emoji( - name: 'singer: light skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F3A4}', - shortName: 'singer_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'rock and roll', - 'fame', - 'artist', - 'disguise', - 'profession', - 'boss', - 'career', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'singer: medium-light skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F3A4}', - shortName: 'singer_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'rock and roll', - 'fame', - 'artist', - 'disguise', - 'profession', - 'boss', - 'career', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'singer: medium skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F3A4}', - shortName: 'singer_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'rock and roll', - 'fame', - 'artist', - 'disguise', - 'profession', - 'boss', - 'career', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'singer: medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F3A4}', - shortName: 'singer_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'rock and roll', - 'fame', - 'artist', - 'disguise', - 'profession', - 'boss', - 'career', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'singer: dark skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F3A4}', - shortName: 'singer_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'rock and roll', - 'fame', - 'artist', - 'disguise', - 'profession', - 'boss', - 'career', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'woman singer', - char: '\u{1F469}\u{200D}\u{1F3A4}', - shortName: 'woman_singer', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'actor', - 'entertainer', - 'rock', - 'singer', - 'star', - 'woman', - 'uc6', - 'instruments', - 'diversity', - 'job', - 'rock and roll', - 'fame', - 'artist', - 'disguise', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'famous', - 'celebrity' - ]), - Emoji( - name: 'woman singer: light skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F3A4}', - shortName: 'woman_singer_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'actor', - 'entertainer', - 'light skin tone', - 'rock', - 'singer', - 'star', - 'woman', - 'uc8', - 'instruments', - 'diversity', - 'job', - 'rock and roll', - 'fame', - 'artist', - 'disguise', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'woman singer: medium-light skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F3A4}', - shortName: 'woman_singer_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'actor', - 'entertainer', - 'medium-light skin tone', - 'rock', - 'singer', - 'star', - 'woman', - 'uc8', - 'instruments', - 'diversity', - 'job', - 'rock and roll', - 'fame', - 'artist', - 'disguise', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'woman singer: medium skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F3A4}', - shortName: 'woman_singer_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'actor', - 'entertainer', - 'medium skin tone', - 'rock', - 'singer', - 'star', - 'woman', - 'uc8', - 'instruments', - 'diversity', - 'job', - 'rock and roll', - 'fame', - 'artist', - 'disguise', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'woman singer: medium-dark skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F3A4}', - shortName: 'woman_singer_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'actor', - 'entertainer', - 'medium-dark skin tone', - 'rock', - 'singer', - 'star', - 'woman', - 'uc8', - 'instruments', - 'diversity', - 'job', - 'rock and roll', - 'fame', - 'artist', - 'disguise', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'woman singer: dark skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F3A4}', - shortName: 'woman_singer_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'actor', - 'dark skin tone', - 'entertainer', - 'rock', - 'singer', - 'star', - 'woman', - 'uc8', - 'instruments', - 'diversity', - 'job', - 'rock and roll', - 'fame', - 'artist', - 'disguise', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'man singer', - char: '\u{1F468}\u{200D}\u{1F3A4}', - shortName: 'man_singer', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'actor', - 'entertainer', - 'man', - 'rock', - 'singer', - 'star', - 'uc6', - 'instruments', - 'diversity', - 'job', - 'rock and roll', - 'fame', - 'artist', - 'disguise', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'famous', - 'celebrity' - ]), - Emoji( - name: 'man singer: light skin tone', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{1F3A4}', - shortName: 'man_singer_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'actor', - 'entertainer', - 'light skin tone', - 'man', - 'rock', - 'singer', - 'star', - 'uc8', - 'instruments', - 'diversity', - 'job', - 'rock and roll', - 'fame', - 'artist', - 'disguise', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'man singer: medium-light skin tone', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{1F3A4}', - shortName: 'man_singer_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'actor', - 'entertainer', - 'man', - 'medium-light skin tone', - 'rock', - 'singer', - 'star', - 'uc8', - 'instruments', - 'diversity', - 'job', - 'rock and roll', - 'fame', - 'artist', - 'disguise', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'man singer: medium skin tone', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{1F3A4}', - shortName: 'man_singer_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'actor', - 'entertainer', - 'man', - 'medium skin tone', - 'rock', - 'singer', - 'star', - 'uc8', - 'instruments', - 'diversity', - 'job', - 'rock and roll', - 'fame', - 'artist', - 'disguise', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'man singer: medium-dark skin tone', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{1F3A4}', - shortName: 'man_singer_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'actor', - 'entertainer', - 'man', - 'medium-dark skin tone', - 'rock', - 'singer', - 'star', - 'uc8', - 'instruments', - 'diversity', - 'job', - 'rock and roll', - 'fame', - 'artist', - 'disguise', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'man singer: dark skin tone', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{1F3A4}', - shortName: 'man_singer_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'actor', - 'dark skin tone', - 'entertainer', - 'man', - 'rock', - 'singer', - 'star', - 'uc8', - 'instruments', - 'diversity', - 'job', - 'rock and roll', - 'fame', - 'artist', - 'disguise', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'teacher', - char: '\u{1F9D1}\u{200D}\u{1F3EB}', - shortName: 'teacher', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'classroom', - 'job', - 'nerd', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ]), - Emoji( - name: 'teacher: light skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F3EB}', - shortName: 'teacher_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'classroom', - 'job', - 'nerd', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'teacher: medium-light skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F3EB}', - shortName: 'teacher_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'classroom', - 'job', - 'nerd', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'teacher: medium skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F3EB}', - shortName: 'teacher_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'classroom', - 'job', - 'nerd', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'teacher: medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F3EB}', - shortName: 'teacher_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'classroom', - 'job', - 'nerd', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'teacher: dark skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F3EB}', - shortName: 'teacher_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'classroom', - 'job', - 'nerd', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'woman teacher', - char: '\u{1F469}\u{200D}\u{1F3EB}', - shortName: 'woman_teacher', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'instructor', - 'professor', - 'teacher', - 'woman', - 'uc6', - 'diversity', - 'classroom', - 'job', - 'nerd', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ]), - Emoji( - name: 'woman teacher: light skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F3EB}', - shortName: 'woman_teacher_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'instructor', - 'light skin tone', - 'professor', - 'teacher', - 'woman', - 'uc8', - 'diversity', - 'classroom', - 'job', - 'nerd', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'woman teacher: medium-light skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F3EB}', - shortName: 'woman_teacher_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'instructor', - 'medium-light skin tone', - 'professor', - 'teacher', - 'woman', - 'uc8', - 'diversity', - 'classroom', - 'job', - 'nerd', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'woman teacher: medium skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F3EB}', - shortName: 'woman_teacher_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'instructor', - 'medium skin tone', - 'professor', - 'teacher', - 'woman', - 'uc8', - 'diversity', - 'classroom', - 'job', - 'nerd', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'woman teacher: medium-dark skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F3EB}', - shortName: 'woman_teacher_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'instructor', - 'medium-dark skin tone', - 'professor', - 'teacher', - 'woman', - 'uc8', - 'diversity', - 'classroom', - 'job', - 'nerd', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'woman teacher: dark skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F3EB}', - shortName: 'woman_teacher_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'instructor', - 'professor', - 'teacher', - 'woman', - 'uc8', - 'diversity', - 'classroom', - 'job', - 'nerd', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'man teacher', - char: '\u{1F468}\u{200D}\u{1F3EB}', - shortName: 'man_teacher', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'instructor', - 'man', - 'professor', - 'teacher', - 'uc6', - 'diversity', - 'classroom', - 'job', - 'nerd', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ]), - Emoji( - name: 'man teacher: light skin tone', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{1F3EB}', - shortName: 'man_teacher_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'instructor', - 'light skin tone', - 'man', - 'professor', - 'teacher', - 'uc8', - 'diversity', - 'classroom', - 'job', - 'nerd', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'man teacher: medium-light skin tone', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{1F3EB}', - shortName: 'man_teacher_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'instructor', - 'man', - 'medium-light skin tone', - 'professor', - 'teacher', - 'uc8', - 'diversity', - 'classroom', - 'job', - 'nerd', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'man teacher: medium skin tone', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{1F3EB}', - shortName: 'man_teacher_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'instructor', - 'man', - 'medium skin tone', - 'professor', - 'teacher', - 'uc8', - 'diversity', - 'classroom', - 'job', - 'nerd', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'man teacher: medium-dark skin tone', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{1F3EB}', - shortName: 'man_teacher_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'instructor', - 'man', - 'medium-dark skin tone', - 'professor', - 'teacher', - 'uc8', - 'diversity', - 'classroom', - 'job', - 'nerd', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'man teacher: dark skin tone', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{1F3EB}', - shortName: 'man_teacher_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'instructor', - 'man', - 'professor', - 'teacher', - 'uc8', - 'diversity', - 'classroom', - 'job', - 'nerd', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'factory worker', - char: '\u{1F9D1}\u{200D}\u{1F3ED}', - shortName: 'factory_worker', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'mask', - 'build', - 'profession', - 'boss', - 'career' - ]), - Emoji( - name: 'factory worker: light skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F3ED}', - shortName: 'factory_worker_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'mask', - 'build', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'factory worker: medium-light skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F3ED}', - shortName: 'factory_worker_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'mask', - 'build', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'factory worker: medium skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F3ED}', - shortName: 'factory_worker_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'mask', - 'build', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'factory worker: medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F3ED}', - shortName: 'factory_worker_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'mask', - 'build', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'factory worker: dark skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F3ED}', - shortName: 'factory_worker_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'mask', - 'build', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'woman factory worker', - char: '\u{1F469}\u{200D}\u{1F3ED}', - shortName: 'woman_factory_worker', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'assembly', - 'factory', - 'industrial', - 'woman', - 'worker', - 'uc6', - 'diversity', - 'job', - 'mask', - 'build', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ]), - Emoji( - name: 'woman factory worker: light skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F3ED}', - shortName: 'woman_factory_worker_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'assembly', - 'factory', - 'industrial', - 'light skin tone', - 'woman', - 'worker', - 'uc8', - 'diversity', - 'job', - 'mask', - 'build', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'woman factory worker: medium-light skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F3ED}', - shortName: 'woman_factory_worker_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'assembly', - 'factory', - 'industrial', - 'medium-light skin tone', - 'woman', - 'worker', - 'uc8', - 'diversity', - 'job', - 'mask', - 'build', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'woman factory worker: medium skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F3ED}', - shortName: 'woman_factory_worker_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'assembly', - 'factory', - 'industrial', - 'medium skin tone', - 'woman', - 'worker', - 'uc8', - 'diversity', - 'job', - 'mask', - 'build', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'woman factory worker: medium-dark skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F3ED}', - shortName: 'woman_factory_worker_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'assembly', - 'factory', - 'industrial', - 'medium-dark skin tone', - 'woman', - 'worker', - 'uc8', - 'diversity', - 'job', - 'mask', - 'build', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'woman factory worker: dark skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F3ED}', - shortName: 'woman_factory_worker_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'assembly', - 'dark skin tone', - 'factory', - 'industrial', - 'woman', - 'worker', - 'uc8', - 'diversity', - 'job', - 'mask', - 'build', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'man factory worker', - char: '\u{1F468}\u{200D}\u{1F3ED}', - shortName: 'man_factory_worker', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'assembly', - 'factory', - 'industrial', - 'man', - 'worker', - 'uc6', - 'diversity', - 'job', - 'mask', - 'build', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ]), - Emoji( - name: 'man factory worker: light skin tone', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{1F3ED}', - shortName: 'man_factory_worker_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'assembly', - 'factory', - 'industrial', - 'light skin tone', - 'man', - 'worker', - 'uc8', - 'diversity', - 'job', - 'mask', - 'build', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'man factory worker: medium-light skin tone', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{1F3ED}', - shortName: 'man_factory_worker_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'assembly', - 'factory', - 'industrial', - 'man', - 'medium-light skin tone', - 'worker', - 'uc8', - 'diversity', - 'job', - 'mask', - 'build', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'man factory worker: medium skin tone', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{1F3ED}', - shortName: 'man_factory_worker_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'assembly', - 'factory', - 'industrial', - 'man', - 'medium skin tone', - 'worker', - 'uc8', - 'diversity', - 'job', - 'mask', - 'build', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'man factory worker: medium-dark skin tone', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{1F3ED}', - shortName: 'man_factory_worker_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'assembly', - 'factory', - 'industrial', - 'man', - 'medium-dark skin tone', - 'worker', - 'uc8', - 'diversity', - 'job', - 'mask', - 'build', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'man factory worker: dark skin tone', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{1F3ED}', - shortName: 'man_factory_worker_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'assembly', - 'dark skin tone', - 'factory', - 'industrial', - 'man', - 'worker', - 'uc8', - 'diversity', - 'job', - 'mask', - 'build', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'technologist', - char: '\u{1F9D1}\u{200D}\u{1F4BB}', - shortName: 'technologist', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'classroom', - 'job', - 'business', - 'nerd', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'office' - ]), - Emoji( - name: 'technologist: light skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F4BB}', - shortName: 'technologist_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'classroom', - 'job', - 'business', - 'nerd', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'office' - ], - modifiable: true), - Emoji( - name: 'technologist: medium-light skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F4BB}', - shortName: 'technologist_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'classroom', - 'job', - 'business', - 'nerd', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'office' - ], - modifiable: true), - Emoji( - name: 'technologist: medium skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F4BB}', - shortName: 'technologist_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'classroom', - 'job', - 'business', - 'nerd', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'office' - ], - modifiable: true), - Emoji( - name: 'technologist: medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F4BB}', - shortName: 'technologist_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'classroom', - 'job', - 'business', - 'nerd', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'office' - ], - modifiable: true), - Emoji( - name: 'technologist: dark skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F4BB}', - shortName: 'technologist_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'classroom', - 'job', - 'business', - 'nerd', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'office' - ], - modifiable: true), - Emoji( - name: 'woman technologist', - char: '\u{1F469}\u{200D}\u{1F4BB}', - shortName: 'woman_technologist', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'coder', - 'developer', - 'inventor', - 'software', - 'technologist', - 'woman', - 'uc6', - 'diversity', - 'classroom', - 'job', - 'business', - 'nerd', - 'code', - 'work', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'coding', - 'office' - ]), - Emoji( - name: 'woman technologist: light skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F4BB}', - shortName: 'woman_technologist_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'coder', - 'developer', - 'inventor', - 'light skin tone', - 'software', - 'technologist', - 'woman', - 'uc8', - 'diversity', - 'classroom', - 'job', - 'business', - 'nerd', - 'code', - 'work', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'coding', - 'office' - ], - modifiable: true), - Emoji( - name: 'woman technologist: medium-light skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F4BB}', - shortName: 'woman_technologist_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'coder', - 'developer', - 'inventor', - 'medium-light skin tone', - 'software', - 'technologist', - 'woman', - 'uc8', - 'diversity', - 'classroom', - 'job', - 'business', - 'nerd', - 'code', - 'work', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'coding', - 'office' - ], - modifiable: true), - Emoji( - name: 'woman technologist: medium skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F4BB}', - shortName: 'woman_technologist_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'coder', - 'developer', - 'inventor', - 'medium skin tone', - 'software', - 'technologist', - 'woman', - 'uc8', - 'diversity', - 'classroom', - 'job', - 'business', - 'nerd', - 'code', - 'work', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'coding', - 'office' - ], - modifiable: true), - Emoji( - name: 'woman technologist: medium-dark skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F4BB}', - shortName: 'woman_technologist_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'coder', - 'developer', - 'inventor', - 'medium-dark skin tone', - 'software', - 'technologist', - 'woman', - 'uc8', - 'diversity', - 'classroom', - 'job', - 'business', - 'nerd', - 'code', - 'work', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'coding', - 'office' - ], - modifiable: true), - Emoji( - name: 'woman technologist: dark skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F4BB}', - shortName: 'woman_technologist_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'coder', - 'dark skin tone', - 'developer', - 'inventor', - 'software', - 'technologist', - 'woman', - 'uc8', - 'diversity', - 'classroom', - 'job', - 'business', - 'nerd', - 'code', - 'work', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'coding', - 'office' - ], - modifiable: true), - Emoji( - name: 'man technologist', - char: '\u{1F468}\u{200D}\u{1F4BB}', - shortName: 'man_technologist', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'coder', - 'developer', - 'inventor', - 'man', - 'software', - 'technologist', - 'uc6', - 'diversity', - 'classroom', - 'job', - 'business', - 'nerd', - 'code', - 'work', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'coding', - 'office' - ]), - Emoji( - name: 'man technologist: light skin tone', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{1F4BB}', - shortName: 'man_technologist_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'coder', - 'developer', - 'inventor', - 'light skin tone', - 'man', - 'software', - 'technologist', - 'uc8', - 'diversity', - 'classroom', - 'job', - 'business', - 'nerd', - 'code', - 'work', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'coding', - 'office' - ], - modifiable: true), - Emoji( - name: 'man technologist: medium-light skin tone', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{1F4BB}', - shortName: 'man_technologist_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'coder', - 'developer', - 'inventor', - 'man', - 'medium-light skin tone', - 'software', - 'technologist', - 'uc8', - 'diversity', - 'classroom', - 'job', - 'business', - 'nerd', - 'code', - 'work', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'coding', - 'office' - ], - modifiable: true), - Emoji( - name: 'man technologist: medium skin tone', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{1F4BB}', - shortName: 'man_technologist_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'coder', - 'developer', - 'inventor', - 'man', - 'medium skin tone', - 'software', - 'technologist', - 'uc8', - 'diversity', - 'classroom', - 'job', - 'business', - 'nerd', - 'code', - 'work', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'coding', - 'office' - ], - modifiable: true), - Emoji( - name: 'man technologist: medium-dark skin tone', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{1F4BB}', - shortName: 'man_technologist_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'coder', - 'developer', - 'inventor', - 'man', - 'medium-dark skin tone', - 'software', - 'technologist', - 'uc8', - 'diversity', - 'classroom', - 'job', - 'business', - 'nerd', - 'code', - 'work', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'coding', - 'office' - ], - modifiable: true), - Emoji( - name: 'man technologist: dark skin tone', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{1F4BB}', - shortName: 'man_technologist_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'coder', - 'dark skin tone', - 'developer', - 'inventor', - 'man', - 'software', - 'technologist', - 'uc8', - 'diversity', - 'classroom', - 'job', - 'business', - 'nerd', - 'code', - 'work', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'coding', - 'office' - ], - modifiable: true), - Emoji( - name: 'office worker', - char: '\u{1F9D1}\u{200D}\u{1F4BC}', - shortName: 'office_worker', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'men', - 'job', - 'business', - 'nerd', - 'work', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'office' - ]), - Emoji( - name: 'office worker: light skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F4BC}', - shortName: 'office_worker_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'men', - 'job', - 'business', - 'nerd', - 'work', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'office' - ], - modifiable: true), - Emoji( - name: 'office worker: medium-light skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F4BC}', - shortName: 'office_worker_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'men', - 'job', - 'business', - 'nerd', - 'work', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'office' - ], - modifiable: true), - Emoji( - name: 'office worker: medium skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F4BC}', - shortName: 'office_worker_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'men', - 'job', - 'business', - 'nerd', - 'work', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'office' - ], - modifiable: true), - Emoji( - name: 'office worker: medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F4BC}', - shortName: 'office_worker_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'men', - 'job', - 'business', - 'nerd', - 'work', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'office' - ], - modifiable: true), - Emoji( - name: 'office worker: dark skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F4BC}', - shortName: 'office_worker_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'men', - 'job', - 'business', - 'nerd', - 'work', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'office' - ], - modifiable: true), - Emoji( - name: 'woman office worker', - char: '\u{1F469}\u{200D}\u{1F4BC}', - shortName: 'woman_office_worker', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'architect', - 'business', - 'manager', - 'office', - 'white-collar', - 'woman', - 'uc6', - 'diversity', - 'women', - 'job', - 'business', - 'nerd', - 'costume', - 'work', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'office' - ]), - Emoji( - name: 'woman office worker: light skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F4BC}', - shortName: 'woman_office_worker_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'architect', - 'business', - 'light skin tone', - 'manager', - 'office', - 'white-collar', - 'woman', - 'uc8', - 'diversity', - 'women', - 'job', - 'business', - 'nerd', - 'costume', - 'work', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'office' - ], - modifiable: true), - Emoji( - name: 'woman office worker: medium-light skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F4BC}', - shortName: 'woman_office_worker_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'architect', - 'business', - 'manager', - 'medium-light skin tone', - 'office', - 'white-collar', - 'woman', - 'uc8', - 'diversity', - 'women', - 'job', - 'business', - 'nerd', - 'costume', - 'work', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'office' - ], - modifiable: true), - Emoji( - name: 'woman office worker: medium skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F4BC}', - shortName: 'woman_office_worker_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'architect', - 'business', - 'manager', - 'medium skin tone', - 'office', - 'white-collar', - 'woman', - 'uc8', - 'diversity', - 'women', - 'job', - 'business', - 'nerd', - 'costume', - 'work', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'office' - ], - modifiable: true), - Emoji( - name: 'woman office worker: medium-dark skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F4BC}', - shortName: 'woman_office_worker_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'architect', - 'business', - 'manager', - 'medium-dark skin tone', - 'office', - 'white-collar', - 'woman', - 'uc8', - 'diversity', - 'women', - 'job', - 'business', - 'nerd', - 'costume', - 'work', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'office' - ], - modifiable: true), - Emoji( - name: 'woman office worker: dark skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F4BC}', - shortName: 'woman_office_worker_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'architect', - 'business', - 'dark skin tone', - 'manager', - 'office', - 'white-collar', - 'woman', - 'uc8', - 'diversity', - 'women', - 'job', - 'business', - 'nerd', - 'costume', - 'work', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'office' - ], - modifiable: true), - Emoji( - name: 'man office worker', - char: '\u{1F468}\u{200D}\u{1F4BC}', - shortName: 'man_office_worker', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'architect', - 'business', - 'man', - 'manager', - 'office', - 'white-collar', - 'uc6', - 'diversity', - 'men', - 'job', - 'business', - 'nerd', - 'work', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'office' - ]), - Emoji( - name: 'man office worker: light skin tone', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{1F4BC}', - shortName: 'man_office_worker_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'architect', - 'business', - 'light skin tone', - 'man', - 'manager', - 'office', - 'white-collar', - 'uc8', - 'diversity', - 'men', - 'job', - 'business', - 'nerd', - 'work', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'office' - ], - modifiable: true), - Emoji( - name: 'man office worker: medium-light skin tone', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{1F4BC}', - shortName: 'man_office_worker_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'architect', - 'business', - 'man', - 'manager', - 'medium-light skin tone', - 'office', - 'white-collar', - 'uc8', - 'diversity', - 'men', - 'job', - 'business', - 'nerd', - 'work', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'office' - ], - modifiable: true), - Emoji( - name: 'man office worker: medium skin tone', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{1F4BC}', - shortName: 'man_office_worker_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'architect', - 'business', - 'man', - 'manager', - 'medium skin tone', - 'office', - 'white-collar', - 'uc8', - 'diversity', - 'men', - 'job', - 'business', - 'nerd', - 'work', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'office' - ], - modifiable: true), - Emoji( - name: 'man office worker: medium-dark skin tone', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{1F4BC}', - shortName: 'man_office_worker_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'architect', - 'business', - 'man', - 'manager', - 'medium-dark skin tone', - 'office', - 'white-collar', - 'uc8', - 'diversity', - 'men', - 'job', - 'business', - 'nerd', - 'work', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'office' - ], - modifiable: true), - Emoji( - name: 'man office worker: dark skin tone', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{1F4BC}', - shortName: 'man_office_worker_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'architect', - 'business', - 'dark skin tone', - 'man', - 'manager', - 'office', - 'white-collar', - 'uc8', - 'diversity', - 'men', - 'job', - 'business', - 'nerd', - 'work', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious', - 'office' - ], - modifiable: true), - Emoji( - name: 'mechanic', - char: '\u{1F9D1}\u{200D}\u{1F527}', - shortName: 'mechanic', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: ['uc12', 'job', 'profession', 'boss', 'career']), - Emoji( - name: 'mechanic: light skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F527}', - shortName: 'mechanic_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: ['uc12', 'job', 'profession', 'boss', 'career'], - modifiable: true), - Emoji( - name: 'mechanic: medium-light skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F527}', - shortName: 'mechanic_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: ['uc12', 'job', 'profession', 'boss', 'career'], - modifiable: true), - Emoji( - name: 'mechanic: medium skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F527}', - shortName: 'mechanic_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: ['uc12', 'job', 'profession', 'boss', 'career'], - modifiable: true), - Emoji( - name: 'mechanic: medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F527}', - shortName: 'mechanic_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: ['uc12', 'job', 'profession', 'boss', 'career'], - modifiable: true), - Emoji( - name: 'mechanic: dark skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F527}', - shortName: 'mechanic_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: ['uc12', 'job', 'profession', 'boss', 'career'], - modifiable: true), - Emoji( - name: 'woman mechanic', - char: '\u{1F469}\u{200D}\u{1F527}', - shortName: 'woman_mechanic', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'electrician', - 'mechanic', - 'plumber', - 'tradesperson', - 'woman', - 'uc6', - 'diversity', - 'job', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ]), - Emoji( - name: 'woman mechanic: light skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F527}', - shortName: 'woman_mechanic_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'electrician', - 'light skin tone', - 'mechanic', - 'plumber', - 'tradesperson', - 'woman', - 'uc8', - 'diversity', - 'job', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'woman mechanic: medium-light skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F527}', - shortName: 'woman_mechanic_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'electrician', - 'mechanic', - 'medium-light skin tone', - 'plumber', - 'tradesperson', - 'woman', - 'uc8', - 'diversity', - 'job', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'woman mechanic: medium skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F527}', - shortName: 'woman_mechanic_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'electrician', - 'mechanic', - 'medium skin tone', - 'plumber', - 'tradesperson', - 'woman', - 'uc8', - 'diversity', - 'job', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'woman mechanic: medium-dark skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F527}', - shortName: 'woman_mechanic_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'electrician', - 'mechanic', - 'medium-dark skin tone', - 'plumber', - 'tradesperson', - 'woman', - 'uc8', - 'diversity', - 'job', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'woman mechanic: dark skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F527}', - shortName: 'woman_mechanic_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'electrician', - 'mechanic', - 'plumber', - 'tradesperson', - 'woman', - 'uc8', - 'diversity', - 'job', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'man mechanic', - char: '\u{1F468}\u{200D}\u{1F527}', - shortName: 'man_mechanic', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'electrician', - 'man', - 'mechanic', - 'plumber', - 'tradesperson', - 'uc6', - 'diversity', - 'job', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ]), - Emoji( - name: 'man mechanic: light skin tone', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{1F527}', - shortName: 'man_mechanic_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'electrician', - 'light skin tone', - 'man', - 'mechanic', - 'plumber', - 'tradesperson', - 'uc8', - 'diversity', - 'job', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'man mechanic: medium-light skin tone', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{1F527}', - shortName: 'man_mechanic_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'electrician', - 'man', - 'mechanic', - 'medium-light skin tone', - 'plumber', - 'tradesperson', - 'uc8', - 'diversity', - 'job', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'man mechanic: medium skin tone', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{1F527}', - shortName: 'man_mechanic_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'electrician', - 'man', - 'mechanic', - 'medium skin tone', - 'plumber', - 'tradesperson', - 'uc8', - 'diversity', - 'job', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'man mechanic: medium-dark skin tone', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{1F527}', - shortName: 'man_mechanic_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'electrician', - 'man', - 'mechanic', - 'medium-dark skin tone', - 'plumber', - 'tradesperson', - 'uc8', - 'diversity', - 'job', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'man mechanic: dark skin tone', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{1F527}', - shortName: 'man_mechanic_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'electrician', - 'man', - 'mechanic', - 'plumber', - 'tradesperson', - 'uc8', - 'diversity', - 'job', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'scientist', - char: '\u{1F9D1}\u{200D}\u{1F52C}', - shortName: 'scientist', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'science', - 'job', - 'nerd', - 'lab', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ]), - Emoji( - name: 'scientist: light skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F52C}', - shortName: 'scientist_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'science', - 'job', - 'nerd', - 'lab', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'scientist: medium-light skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F52C}', - shortName: 'scientist_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'science', - 'job', - 'nerd', - 'lab', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'scientist: medium skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F52C}', - shortName: 'scientist_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'science', - 'job', - 'nerd', - 'lab', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'scientist: medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F52C}', - shortName: 'scientist_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'science', - 'job', - 'nerd', - 'lab', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'scientist: dark skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F52C}', - shortName: 'scientist_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'science', - 'job', - 'nerd', - 'lab', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'woman scientist', - char: '\u{1F469}\u{200D}\u{1F52C}', - shortName: 'woman_scientist', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'biologist', - 'chemist', - 'engineer', - 'mathematician', - 'physicist', - 'scientist', - 'woman', - 'uc6', - 'diversity', - 'science', - 'job', - 'nerd', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'lab', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ]), - Emoji( - name: 'woman scientist: light skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F52C}', - shortName: 'woman_scientist_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'biologist', - 'chemist', - 'engineer', - 'light skin tone', - 'mathematician', - 'physicist', - 'scientist', - 'woman', - 'uc8', - 'diversity', - 'science', - 'job', - 'nerd', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'lab', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'woman scientist: medium-light skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F52C}', - shortName: 'woman_scientist_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'biologist', - 'chemist', - 'engineer', - 'mathematician', - 'medium-light skin tone', - 'physicist', - 'scientist', - 'woman', - 'uc8', - 'diversity', - 'science', - 'job', - 'nerd', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'lab', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'woman scientist: medium skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F52C}', - shortName: 'woman_scientist_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'biologist', - 'chemist', - 'engineer', - 'mathematician', - 'medium skin tone', - 'physicist', - 'scientist', - 'woman', - 'uc8', - 'diversity', - 'science', - 'job', - 'nerd', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'lab', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'woman scientist: medium-dark skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F52C}', - shortName: 'woman_scientist_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'biologist', - 'chemist', - 'engineer', - 'mathematician', - 'medium-dark skin tone', - 'physicist', - 'scientist', - 'woman', - 'uc8', - 'diversity', - 'science', - 'job', - 'nerd', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'lab', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'woman scientist: dark skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F52C}', - shortName: 'woman_scientist_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'biologist', - 'chemist', - 'dark skin tone', - 'engineer', - 'mathematician', - 'physicist', - 'scientist', - 'woman', - 'uc8', - 'diversity', - 'science', - 'job', - 'nerd', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'lab', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'man scientist', - char: '\u{1F468}\u{200D}\u{1F52C}', - shortName: 'man_scientist', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'biologist', - 'chemist', - 'engineer', - 'man', - 'mathematician', - 'physicist', - 'scientist', - 'uc6', - 'diversity', - 'science', - 'job', - 'nerd', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'lab', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ]), - Emoji( - name: 'man scientist: light skin tone', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{1F52C}', - shortName: 'man_scientist_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'biologist', - 'chemist', - 'engineer', - 'light skin tone', - 'man', - 'mathematician', - 'physicist', - 'scientist', - 'uc8', - 'diversity', - 'science', - 'job', - 'nerd', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'lab', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'man scientist: medium-light skin tone', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{1F52C}', - shortName: 'man_scientist_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'biologist', - 'chemist', - 'engineer', - 'man', - 'mathematician', - 'medium-light skin tone', - 'physicist', - 'scientist', - 'uc8', - 'diversity', - 'science', - 'job', - 'nerd', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'lab', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'man scientist: medium skin tone', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{1F52C}', - shortName: 'man_scientist_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'biologist', - 'chemist', - 'engineer', - 'man', - 'mathematician', - 'medium skin tone', - 'physicist', - 'scientist', - 'uc8', - 'diversity', - 'science', - 'job', - 'nerd', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'lab', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'man scientist: medium-dark skin tone', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{1F52C}', - shortName: 'man_scientist_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'biologist', - 'chemist', - 'engineer', - 'man', - 'mathematician', - 'medium-dark skin tone', - 'physicist', - 'scientist', - 'uc8', - 'diversity', - 'science', - 'job', - 'nerd', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'lab', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'man scientist: dark skin tone', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{1F52C}', - shortName: 'man_scientist_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'biologist', - 'chemist', - 'dark skin tone', - 'engineer', - 'man', - 'mathematician', - 'physicist', - 'scientist', - 'uc8', - 'diversity', - 'science', - 'job', - 'nerd', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'lab', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'artist', - char: '\u{1F9D1}\u{200D}\u{1F3A8}', - shortName: 'artist', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'painting', - 'artist', - 'profession', - 'boss', - 'career', - 'painter', - 'arts' - ]), - Emoji( - name: 'artist: light skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F3A8}', - shortName: 'artist_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'painting', - 'artist', - 'profession', - 'boss', - 'career', - 'painter', - 'arts' - ], - modifiable: true), - Emoji( - name: 'artist: medium-light skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F3A8}', - shortName: 'artist_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'painting', - 'artist', - 'profession', - 'boss', - 'career', - 'painter', - 'arts' - ], - modifiable: true), - Emoji( - name: 'artist: medium skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F3A8}', - shortName: 'artist_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'painting', - 'artist', - 'profession', - 'boss', - 'career', - 'painter', - 'arts' - ], - modifiable: true), - Emoji( - name: 'artist: medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F3A8}', - shortName: 'artist_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'painting', - 'artist', - 'profession', - 'boss', - 'career', - 'painter', - 'arts' - ], - modifiable: true), - Emoji( - name: 'artist: dark skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F3A8}', - shortName: 'artist_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'painting', - 'artist', - 'profession', - 'boss', - 'career', - 'painter', - 'arts' - ], - modifiable: true), - Emoji( - name: 'woman artist', - char: '\u{1F469}\u{200D}\u{1F3A8}', - shortName: 'woman_artist', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'artist', - 'palette', - 'woman', - 'uc6', - 'diversity', - 'job', - 'painting', - 'artist', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'painter', - 'arts' - ]), - Emoji( - name: 'woman artist: light skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F3A8}', - shortName: 'woman_artist_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'artist', - 'light skin tone', - 'palette', - 'woman', - 'uc8', - 'diversity', - 'job', - 'painting', - 'artist', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'painter', - 'arts' - ], - modifiable: true), - Emoji( - name: 'woman artist: medium-light skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F3A8}', - shortName: 'woman_artist_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'artist', - 'medium-light skin tone', - 'palette', - 'woman', - 'uc8', - 'diversity', - 'job', - 'painting', - 'artist', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'painter', - 'arts' - ], - modifiable: true), - Emoji( - name: 'woman artist: medium skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F3A8}', - shortName: 'woman_artist_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'artist', - 'medium skin tone', - 'palette', - 'woman', - 'uc8', - 'diversity', - 'job', - 'painting', - 'artist', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'painter', - 'arts' - ], - modifiable: true), - Emoji( - name: 'woman artist: medium-dark skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F3A8}', - shortName: 'woman_artist_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'artist', - 'medium-dark skin tone', - 'palette', - 'woman', - 'uc8', - 'diversity', - 'job', - 'painting', - 'artist', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'painter', - 'arts' - ], - modifiable: true), - Emoji( - name: 'woman artist: dark skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F3A8}', - shortName: 'woman_artist_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'artist', - 'dark skin tone', - 'palette', - 'woman', - 'uc8', - 'diversity', - 'job', - 'painting', - 'artist', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'painter', - 'arts' - ], - modifiable: true), - Emoji( - name: 'man artist', - char: '\u{1F468}\u{200D}\u{1F3A8}', - shortName: 'man_artist', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'artist', - 'man', - 'palette', - 'uc6', - 'diversity', - 'job', - 'painting', - 'artist', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'painter', - 'arts' - ]), - Emoji( - name: 'man artist: light skin tone', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{1F3A8}', - shortName: 'man_artist_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'artist', - 'light skin tone', - 'man', - 'palette', - 'uc8', - 'diversity', - 'job', - 'painting', - 'artist', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'painter', - 'arts' - ], - modifiable: true), - Emoji( - name: 'man artist: medium-light skin tone', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{1F3A8}', - shortName: 'man_artist_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'artist', - 'man', - 'medium-light skin tone', - 'palette', - 'uc8', - 'diversity', - 'job', - 'painting', - 'artist', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'painter', - 'arts' - ], - modifiable: true), - Emoji( - name: 'man artist: medium skin tone', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{1F3A8}', - shortName: 'man_artist_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'artist', - 'man', - 'medium skin tone', - 'palette', - 'uc8', - 'diversity', - 'job', - 'painting', - 'artist', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'painter', - 'arts' - ], - modifiable: true), - Emoji( - name: 'man artist: medium-dark skin tone', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{1F3A8}', - shortName: 'man_artist_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'artist', - 'man', - 'medium-dark skin tone', - 'palette', - 'uc8', - 'diversity', - 'job', - 'painting', - 'artist', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'painter', - 'arts' - ], - modifiable: true), - Emoji( - name: 'man artist: dark skin tone', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{1F3A8}', - shortName: 'man_artist_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'artist', - 'dark skin tone', - 'man', - 'palette', - 'uc8', - 'diversity', - 'job', - 'painting', - 'artist', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'painter', - 'arts' - ], - modifiable: true), - Emoji( - name: 'firefighter', - char: '\u{1F9D1}\u{200D}\u{1F692}', - shortName: 'firefighter', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - '911', - 'help', - 'handsome', - 'fires', - 'helmet', - 'disguise', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'stud' - ]), - Emoji( - name: 'firefighter: light skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F692}', - shortName: 'firefighter_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - '911', - 'help', - 'handsome', - 'fires', - 'helmet', - 'disguise', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'stud' - ], - modifiable: true), - Emoji( - name: 'firefighter: medium-light skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F692}', - shortName: 'firefighter_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - '911', - 'help', - 'handsome', - 'fires', - 'helmet', - 'disguise', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'stud' - ], - modifiable: true), - Emoji( - name: 'firefighter: medium skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F692}', - shortName: 'firefighter_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - '911', - 'help', - 'handsome', - 'fires', - 'helmet', - 'disguise', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'stud' - ], - modifiable: true), - Emoji( - name: 'firefighter: medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F692}', - shortName: 'firefighter_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - '911', - 'help', - 'handsome', - 'fires', - 'helmet', - 'disguise', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'stud' - ], - modifiable: true), - Emoji( - name: 'firefighter: dark skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F692}', - shortName: 'firefighter_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - '911', - 'help', - 'handsome', - 'fires', - 'helmet', - 'disguise', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'stud' - ], - modifiable: true), - Emoji( - name: 'woman firefighter', - char: '\u{1F469}\u{200D}\u{1F692}', - shortName: 'woman_firefighter', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'firefighter', - 'firetruck', - 'woman', - 'uc6', - 'diversity', - 'job', - '911', - 'help', - 'fires', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury' - ]), - Emoji( - name: 'woman firefighter: light skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F692}', - shortName: 'woman_firefighter_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'firefighter', - 'firetruck', - 'light skin tone', - 'woman', - 'uc8', - 'diversity', - 'job', - '911', - 'help', - 'fires', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury' - ], - modifiable: true), - Emoji( - name: 'woman firefighter: medium-light skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F692}', - shortName: 'woman_firefighter_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'firefighter', - 'firetruck', - 'medium-light skin tone', - 'woman', - 'uc8', - 'diversity', - 'job', - '911', - 'help', - 'fires', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury' - ], - modifiable: true), - Emoji( - name: 'woman firefighter: medium skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F692}', - shortName: 'woman_firefighter_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'firefighter', - 'firetruck', - 'medium skin tone', - 'woman', - 'uc8', - 'diversity', - 'job', - '911', - 'help', - 'fires', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury' - ], - modifiable: true), - Emoji( - name: 'woman firefighter: medium-dark skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F692}', - shortName: 'woman_firefighter_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'firefighter', - 'firetruck', - 'medium-dark skin tone', - 'woman', - 'uc8', - 'diversity', - 'job', - '911', - 'help', - 'fires', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury' - ], - modifiable: true), - Emoji( - name: 'woman firefighter: dark skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F692}', - shortName: 'woman_firefighter_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'firefighter', - 'firetruck', - 'woman', - 'uc8', - 'diversity', - 'job', - '911', - 'help', - 'fires', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury' - ], - modifiable: true), - Emoji( - name: 'man firefighter', - char: '\u{1F468}\u{200D}\u{1F692}', - shortName: 'man_firefighter', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'firefighter', - 'firetruck', - 'man', - 'uc6', - 'diversity', - 'job', - '911', - 'help', - 'handsome', - 'fires', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'stud' - ]), - Emoji( - name: 'man firefighter: light skin tone', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{1F692}', - shortName: 'man_firefighter_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'firefighter', - 'firetruck', - 'light skin tone', - 'man', - 'uc8', - 'diversity', - 'job', - '911', - 'help', - 'handsome', - 'fires', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man firefighter: medium-light skin tone', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{1F692}', - shortName: 'man_firefighter_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'firefighter', - 'firetruck', - 'man', - 'medium-light skin tone', - 'uc8', - 'diversity', - 'job', - '911', - 'help', - 'handsome', - 'fires', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man firefighter: medium skin tone', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{1F692}', - shortName: 'man_firefighter_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'firefighter', - 'firetruck', - 'man', - 'medium skin tone', - 'uc8', - 'diversity', - 'job', - '911', - 'help', - 'handsome', - 'fires', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man firefighter: medium-dark skin tone', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{1F692}', - shortName: 'man_firefighter_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'firefighter', - 'firetruck', - 'man', - 'medium-dark skin tone', - 'uc8', - 'diversity', - 'job', - '911', - 'help', - 'handsome', - 'fires', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man firefighter: dark skin tone', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{1F692}', - shortName: 'man_firefighter_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'firefighter', - 'firetruck', - 'man', - 'uc8', - 'diversity', - 'job', - '911', - 'help', - 'handsome', - 'fires', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury', - 'stud' - ], - modifiable: true), - Emoji( - name: 'pilot', - char: '\u{1F9D1}\u{200D}\u{2708}\u{FE0F}', - shortName: 'pilot', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'fly', - 'job', - 'airplane', - 'handsome', - 'disguise', - 'flight', - 'flying', - 'flights', - 'avion', - 'profession', - 'boss', - 'career', - 'airline', - 'aircraft', - 'airforce', - 'air force', - 'airport', - 'stud' - ]), - Emoji( - name: 'pilot: light skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{2708}\u{FE0F}', - shortName: 'pilot_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'fly', - 'job', - 'airplane', - 'handsome', - 'disguise', - 'flight', - 'flying', - 'flights', - 'avion', - 'profession', - 'boss', - 'career', - 'airline', - 'aircraft', - 'airforce', - 'air force', - 'airport', - 'stud' - ], - modifiable: true), - Emoji( - name: 'pilot: medium-light skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{2708}\u{FE0F}', - shortName: 'pilot_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'fly', - 'job', - 'airplane', - 'handsome', - 'disguise', - 'flight', - 'flying', - 'flights', - 'avion', - 'profession', - 'boss', - 'career', - 'airline', - 'aircraft', - 'airforce', - 'air force', - 'airport', - 'stud' - ], - modifiable: true), - Emoji( - name: 'pilot: medium skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{2708}\u{FE0F}', - shortName: 'pilot_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'fly', - 'job', - 'airplane', - 'handsome', - 'disguise', - 'flight', - 'flying', - 'flights', - 'avion', - 'profession', - 'boss', - 'career', - 'airline', - 'aircraft', - 'airforce', - 'air force', - 'airport', - 'stud' - ], - modifiable: true), - Emoji( - name: 'pilot: medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{2708}\u{FE0F}', - shortName: 'pilot_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'fly', - 'job', - 'airplane', - 'handsome', - 'disguise', - 'flight', - 'flying', - 'flights', - 'avion', - 'profession', - 'boss', - 'career', - 'airline', - 'aircraft', - 'airforce', - 'air force', - 'airport', - 'stud' - ], - modifiable: true), - Emoji( - name: 'pilot: dark skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{2708}\u{FE0F}', - shortName: 'pilot_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'fly', - 'job', - 'airplane', - 'handsome', - 'disguise', - 'flight', - 'flying', - 'flights', - 'avion', - 'profession', - 'boss', - 'career', - 'airline', - 'aircraft', - 'airforce', - 'air force', - 'airport', - 'stud' - ], - modifiable: true), - Emoji( - name: 'woman pilot', - char: '\u{1F469}\u{200D}\u{2708}\u{FE0F}', - shortName: 'woman_pilot', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'pilot', - 'plane', - 'woman', - 'uc6', - 'diversity', - 'fly', - 'job', - 'airplane', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'flight', - 'flying', - 'flights', - 'avion', - 'profession', - 'boss', - 'career', - 'airline', - 'aircraft', - 'airforce', - 'air force', - 'airport' - ]), - Emoji( - name: 'woman pilot: light skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{2708}\u{FE0F}', - shortName: 'woman_pilot_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'light skin tone', - 'pilot', - 'plane', - 'woman', - 'uc8', - 'diversity', - 'fly', - 'job', - 'airplane', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'flight', - 'flying', - 'flights', - 'avion', - 'profession', - 'boss', - 'career', - 'airline', - 'aircraft', - 'airforce', - 'air force', - 'airport' - ], - modifiable: true), - Emoji( - name: 'woman pilot: medium-light skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{2708}\u{FE0F}', - shortName: 'woman_pilot_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'medium-light skin tone', - 'pilot', - 'plane', - 'woman', - 'uc8', - 'diversity', - 'fly', - 'job', - 'airplane', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'flight', - 'flying', - 'flights', - 'avion', - 'profession', - 'boss', - 'career', - 'airline', - 'aircraft', - 'airforce', - 'air force', - 'airport' - ], - modifiable: true), - Emoji( - name: 'woman pilot: medium skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{2708}\u{FE0F}', - shortName: 'woman_pilot_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'medium skin tone', - 'pilot', - 'plane', - 'woman', - 'uc8', - 'diversity', - 'fly', - 'job', - 'airplane', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'flight', - 'flying', - 'flights', - 'avion', - 'profession', - 'boss', - 'career', - 'airline', - 'aircraft', - 'airforce', - 'air force', - 'airport' - ], - modifiable: true), - Emoji( - name: 'woman pilot: medium-dark skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{2708}\u{FE0F}', - shortName: 'woman_pilot_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'medium-dark skin tone', - 'pilot', - 'plane', - 'woman', - 'uc8', - 'diversity', - 'fly', - 'job', - 'airplane', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'flight', - 'flying', - 'flights', - 'avion', - 'profession', - 'boss', - 'career', - 'airline', - 'aircraft', - 'airforce', - 'air force', - 'airport' - ], - modifiable: true), - Emoji( - name: 'woman pilot: dark skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{2708}\u{FE0F}', - shortName: 'woman_pilot_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'pilot', - 'plane', - 'woman', - 'uc8', - 'diversity', - 'fly', - 'job', - 'airplane', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'flight', - 'flying', - 'flights', - 'avion', - 'profession', - 'boss', - 'career', - 'airline', - 'aircraft', - 'airforce', - 'air force', - 'airport' - ], - modifiable: true), - Emoji( - name: 'man pilot', - char: '\u{1F468}\u{200D}\u{2708}\u{FE0F}', - shortName: 'man_pilot', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'man', - 'pilot', - 'plane', - 'uc6', - 'diversity', - 'fly', - 'job', - 'airplane', - 'handsome', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'flight', - 'flying', - 'flights', - 'avion', - 'profession', - 'boss', - 'career', - 'airline', - 'aircraft', - 'airforce', - 'air force', - 'airport', - 'stud' - ]), - Emoji( - name: 'man pilot: light skin tone', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{2708}\u{FE0F}', - shortName: 'man_pilot_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'light skin tone', - 'man', - 'pilot', - 'plane', - 'uc8', - 'diversity', - 'fly', - 'job', - 'airplane', - 'handsome', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'flight', - 'flying', - 'flights', - 'avion', - 'profession', - 'boss', - 'career', - 'airline', - 'aircraft', - 'airforce', - 'air force', - 'airport', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man pilot: medium-light skin tone', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{2708}\u{FE0F}', - shortName: 'man_pilot_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'man', - 'medium-light skin tone', - 'pilot', - 'plane', - 'uc8', - 'diversity', - 'fly', - 'job', - 'airplane', - 'handsome', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'flight', - 'flying', - 'flights', - 'avion', - 'profession', - 'boss', - 'career', - 'airline', - 'aircraft', - 'airforce', - 'air force', - 'airport', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man pilot: medium skin tone', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{2708}\u{FE0F}', - shortName: 'man_pilot_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'man', - 'medium skin tone', - 'pilot', - 'plane', - 'uc8', - 'diversity', - 'fly', - 'job', - 'airplane', - 'handsome', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'flight', - 'flying', - 'flights', - 'avion', - 'profession', - 'boss', - 'career', - 'airline', - 'aircraft', - 'airforce', - 'air force', - 'airport', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man pilot: medium-dark skin tone', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{2708}\u{FE0F}', - shortName: 'man_pilot_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'man', - 'medium-dark skin tone', - 'pilot', - 'plane', - 'uc8', - 'diversity', - 'fly', - 'job', - 'airplane', - 'handsome', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'flight', - 'flying', - 'flights', - 'avion', - 'profession', - 'boss', - 'career', - 'airline', - 'aircraft', - 'airforce', - 'air force', - 'airport', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man pilot: dark skin tone', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{2708}\u{FE0F}', - shortName: 'man_pilot_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'man', - 'pilot', - 'plane', - 'uc8', - 'diversity', - 'fly', - 'job', - 'airplane', - 'handsome', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'flight', - 'flying', - 'flights', - 'avion', - 'profession', - 'boss', - 'career', - 'airline', - 'aircraft', - 'airforce', - 'air force', - 'airport', - 'stud' - ], - modifiable: true), - Emoji( - name: 'astronaut', - char: '\u{1F9D1}\u{200D}\u{1F680}', - shortName: 'astronaut', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'space', - 'job', - 'helmet', - 'disguise', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'profession', - 'boss', - 'career' - ]), - Emoji( - name: 'astronaut: light skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F680}', - shortName: 'astronaut_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'space', - 'job', - 'helmet', - 'disguise', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'astronaut: medium-light skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F680}', - shortName: 'astronaut_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'space', - 'job', - 'helmet', - 'disguise', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'astronaut: medium skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F680}', - shortName: 'astronaut_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'space', - 'job', - 'helmet', - 'disguise', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'astronaut: medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F680}', - shortName: 'astronaut_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'space', - 'job', - 'helmet', - 'disguise', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'astronaut: dark skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F680}', - shortName: 'astronaut_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'space', - 'job', - 'helmet', - 'disguise', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'woman astronaut', - char: '\u{1F469}\u{200D}\u{1F680}', - shortName: 'woman_astronaut', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'astronaut', - 'rocket', - 'woman', - 'uc6', - 'diversity', - 'space', - 'job', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'profession', - 'boss', - 'career' - ]), - Emoji( - name: 'woman astronaut: light skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F680}', - shortName: 'woman_astronaut_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'astronaut', - 'light skin tone', - 'rocket', - 'woman', - 'uc8', - 'diversity', - 'space', - 'job', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'woman astronaut: medium-light skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F680}', - shortName: 'woman_astronaut_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'astronaut', - 'medium-light skin tone', - 'rocket', - 'woman', - 'uc8', - 'diversity', - 'space', - 'job', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'woman astronaut: medium skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F680}', - shortName: 'woman_astronaut_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'astronaut', - 'medium skin tone', - 'rocket', - 'woman', - 'uc8', - 'diversity', - 'space', - 'job', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'woman astronaut: medium-dark skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F680}', - shortName: 'woman_astronaut_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'astronaut', - 'medium-dark skin tone', - 'rocket', - 'woman', - 'uc8', - 'diversity', - 'space', - 'job', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'woman astronaut: dark skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F680}', - shortName: 'woman_astronaut_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'astronaut', - 'dark skin tone', - 'rocket', - 'woman', - 'uc8', - 'diversity', - 'space', - 'job', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'man astronaut', - char: '\u{1F468}\u{200D}\u{1F680}', - shortName: 'man_astronaut', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'astronaut', - 'man', - 'rocket', - 'uc6', - 'diversity', - 'space', - 'job', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'profession', - 'boss', - 'career' - ]), - Emoji( - name: 'man astronaut: light skin tone', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{1F680}', - shortName: 'man_astronaut_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'astronaut', - 'light skin tone', - 'man', - 'rocket', - 'uc8', - 'diversity', - 'space', - 'job', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'man astronaut: medium-light skin tone', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{1F680}', - shortName: 'man_astronaut_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'astronaut', - 'man', - 'medium-light skin tone', - 'rocket', - 'uc8', - 'diversity', - 'space', - 'job', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'man astronaut: medium skin tone', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{1F680}', - shortName: 'man_astronaut_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'astronaut', - 'man', - 'medium skin tone', - 'rocket', - 'uc8', - 'diversity', - 'space', - 'job', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'man astronaut: medium-dark skin tone', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{1F680}', - shortName: 'man_astronaut_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'astronaut', - 'man', - 'medium-dark skin tone', - 'rocket', - 'uc8', - 'diversity', - 'space', - 'job', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'man astronaut: dark skin tone', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{1F680}', - shortName: 'man_astronaut_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'astronaut', - 'dark skin tone', - 'man', - 'rocket', - 'uc8', - 'diversity', - 'space', - 'job', - 'helmet', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'profession', - 'boss', - 'career' - ], - modifiable: true), - Emoji( - name: 'judge', - char: '\u{1F9D1}\u{200D}\u{2696}\u{FE0F}', - shortName: 'judge', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'nerd', - 'court', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ]), - Emoji( - name: 'judge: light skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{2696}\u{FE0F}', - shortName: 'judge_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'nerd', - 'court', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'judge: medium-light skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{2696}\u{FE0F}', - shortName: 'judge_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'nerd', - 'court', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'judge: medium skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{2696}\u{FE0F}', - shortName: 'judge_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'nerd', - 'court', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'judge: medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{2696}\u{FE0F}', - shortName: 'judge_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'nerd', - 'court', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'judge: dark skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{2696}\u{FE0F}', - shortName: 'judge_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc12', - 'job', - 'nerd', - 'court', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'woman judge', - char: '\u{1F469}\u{200D}\u{2696}\u{FE0F}', - shortName: 'woman_judge', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'judge', - 'scales', - 'woman', - 'uc6', - 'diversity', - 'job', - 'nerd', - 'court', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ]), - Emoji( - name: 'woman judge: light skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{2696}\u{FE0F}', - shortName: 'woman_judge_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'judge', - 'light skin tone', - 'scales', - 'woman', - 'uc8', - 'diversity', - 'job', - 'nerd', - 'court', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'woman judge: medium-light skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{2696}\u{FE0F}', - shortName: 'woman_judge_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'judge', - 'medium-light skin tone', - 'scales', - 'woman', - 'uc8', - 'diversity', - 'job', - 'nerd', - 'court', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'woman judge: medium skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{2696}\u{FE0F}', - shortName: 'woman_judge_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'judge', - 'medium skin tone', - 'scales', - 'woman', - 'uc8', - 'diversity', - 'job', - 'nerd', - 'court', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'woman judge: medium-dark skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{2696}\u{FE0F}', - shortName: 'woman_judge_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'judge', - 'medium-dark skin tone', - 'scales', - 'woman', - 'uc8', - 'diversity', - 'job', - 'nerd', - 'court', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'woman judge: dark skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{2696}\u{FE0F}', - shortName: 'woman_judge_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'judge', - 'scales', - 'woman', - 'uc8', - 'diversity', - 'job', - 'nerd', - 'court', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'man judge', - char: '\u{1F468}\u{200D}\u{2696}\u{FE0F}', - shortName: 'man_judge', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'justice', - 'man', - 'scales', - 'uc6', - 'diversity', - 'job', - 'nerd', - 'court', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ]), - Emoji( - name: 'man judge: light skin tone', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{2696}\u{FE0F}', - shortName: 'man_judge_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'justice', - 'light skin tone', - 'man', - 'scales', - 'uc8', - 'diversity', - 'job', - 'nerd', - 'court', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'man judge: medium-light skin tone', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{2696}\u{FE0F}', - shortName: 'man_judge_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'justice', - 'man', - 'medium-light skin tone', - 'scales', - 'uc8', - 'diversity', - 'job', - 'nerd', - 'court', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'man judge: medium skin tone', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{2696}\u{FE0F}', - shortName: 'man_judge_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'justice', - 'man', - 'medium skin tone', - 'scales', - 'uc8', - 'diversity', - 'job', - 'nerd', - 'court', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'man judge: medium-dark skin tone', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{2696}\u{FE0F}', - shortName: 'man_judge_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'justice', - 'man', - 'medium-dark skin tone', - 'scales', - 'uc8', - 'diversity', - 'job', - 'nerd', - 'court', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'man judge: dark skin tone', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{2696}\u{FE0F}', - shortName: 'man_judge_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'justice', - 'man', - 'scales', - 'uc8', - 'diversity', - 'job', - 'nerd', - 'court', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'profession', - 'boss', - 'career', - 'smart', - 'geek', - 'serious' - ], - modifiable: true), - Emoji( - name: 'person with veil', - char: '\u{1F470}', - shortName: 'person_with_veil', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'person', - 'veil', - 'wedding', - 'uc6', - 'diversity', - 'wedding', - 'women', - 'beautiful', - 'las vegas', - 'wife', - 'dress', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'vegas' - ]), - Emoji( - name: 'person with veil: light skin tone', - char: '\u{1F470}\u{1F3FB}', - shortName: 'person_with_veil_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'person', - 'light skin tone', - 'veil', - 'wedding', - 'uc8', - 'diversity', - 'wedding', - 'women', - 'beautiful', - 'las vegas', - 'wife', - 'dress', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'vegas' - ], - modifiable: true), - Emoji( - name: 'person with veil: medium-light skin tone', - char: '\u{1F470}\u{1F3FC}', - shortName: 'person_with_veil_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'person', - 'medium-light skin tone', - 'veil', - 'wedding', - 'uc8', - 'diversity', - 'wedding', - 'women', - 'beautiful', - 'las vegas', - 'wife', - 'dress', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'vegas' - ], - modifiable: true), - Emoji( - name: 'person with veil: medium skin tone', - char: '\u{1F470}\u{1F3FD}', - shortName: 'person_with_veil_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'person', - 'medium skin tone', - 'veil', - 'wedding', - 'uc8', - 'diversity', - 'wedding', - 'women', - 'beautiful', - 'las vegas', - 'wife', - 'dress', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'vegas' - ], - modifiable: true), - Emoji( - name: 'person with veil: medium-dark skin tone', - char: '\u{1F470}\u{1F3FE}', - shortName: 'person_with_veil_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'person', - 'medium-dark skin tone', - 'veil', - 'wedding', - 'uc8', - 'diversity', - 'wedding', - 'women', - 'beautiful', - 'las vegas', - 'wife', - 'dress', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'vegas' - ], - modifiable: true), - Emoji( - name: 'person with veil: dark skin tone', - char: '\u{1F470}\u{1F3FF}', - shortName: 'person_with_veil_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'person', - 'dark skin tone', - 'veil', - 'wedding', - 'uc8', - 'diversity', - 'wedding', - 'women', - 'beautiful', - 'las vegas', - 'wife', - 'dress', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'vegas' - ], - modifiable: true), - Emoji( - name: 'woman with veil', - char: '\u{1F470}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_with_veil', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'wedding', - 'beautiful', - 'wife', - 'dress', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely' - ]), - Emoji( - name: 'woman with veil: light skin tone', - char: '\u{1F470}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_with_veil_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'wedding', - 'beautiful', - 'wife', - 'dress', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely' - ], - modifiable: true), - Emoji( - name: 'woman with veil: medium-light skin tone', - char: '\u{1F470}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_with_veil_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'wedding', - 'beautiful', - 'wife', - 'dress', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely' - ], - modifiable: true), - Emoji( - name: 'woman with veil: medium skin tone', - char: '\u{1F470}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_with_veil_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'wedding', - 'beautiful', - 'wife', - 'dress', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely' - ], - modifiable: true), - Emoji( - name: 'woman with veil: medium-dark skin tone', - char: '\u{1F470}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_with_veil_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'wedding', - 'beautiful', - 'wife', - 'dress', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely' - ], - modifiable: true), - Emoji( - name: 'woman with veil: dark skin tone', - char: '\u{1F470}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_with_veil_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'wedding', - 'beautiful', - 'wife', - 'dress', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely' - ], - modifiable: true), - Emoji( - name: 'man with veil', - char: '\u{1F470}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_with_veil', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'wedding', - 'wife', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry' - ]), - Emoji( - name: 'man with veil: light skin tone', - char: '\u{1F470}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_with_veil_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'wedding', - 'wife', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry' - ], - modifiable: true), - Emoji( - name: 'man with veil: medium-light skin tone', - char: '\u{1F470}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_with_veil_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'wedding', - 'wife', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry' - ], - modifiable: true), - Emoji( - name: 'man with veil: medium skin tone', - char: '\u{1F470}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_with_veil_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'wedding', - 'wife', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry' - ], - modifiable: true), - Emoji( - name: 'man with veil: medium-dark skin tone', - char: '\u{1F470}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_with_veil_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'wedding', - 'wife', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry' - ], - modifiable: true), - Emoji( - name: 'man with veil: dark skin tone', - char: '\u{1F470}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_with_veil_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'wedding', - 'wife', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry' - ], - modifiable: true), - Emoji( - name: 'person in tuxedo', - char: '\u{1F935}', - shortName: 'person_in_tuxedo', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'person', - 'tuxedo', - 'uc9', - 'diversity', - 'wedding', - 'men', - 'boys night', - 'donald trump', - 'fame', - 'vampire', - 'las vegas', - 'rich', - 'jacket', - 'handsome', - 'costume', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'trump', - 'famous', - 'celebrity', - 'dracula', - 'vegas', - 'grand', - 'expensive', - 'fancy', - 'veste', - 'stud' - ]), - Emoji( - name: 'person in tuxedo: light skin tone', - char: '\u{1F935}\u{1F3FB}', - shortName: 'person_in_tuxedo_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'light skin tone', - 'person', - 'tuxedo', - 'uc9', - 'diversity', - 'wedding', - 'men', - 'boys night', - 'donald trump', - 'fame', - 'vampire', - 'las vegas', - 'rich', - 'jacket', - 'handsome', - 'costume', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'trump', - 'famous', - 'celebrity', - 'dracula', - 'vegas', - 'grand', - 'expensive', - 'fancy', - 'veste', - 'stud' - ], - modifiable: true), - Emoji( - name: 'person in tuxedo: medium-light skin tone', - char: '\u{1F935}\u{1F3FC}', - shortName: 'person_in_tuxedo_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'person', - 'medium-light skin tone', - 'tuxedo', - 'uc9', - 'diversity', - 'wedding', - 'men', - 'boys night', - 'donald trump', - 'fame', - 'vampire', - 'las vegas', - 'rich', - 'jacket', - 'handsome', - 'costume', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'trump', - 'famous', - 'celebrity', - 'dracula', - 'vegas', - 'grand', - 'expensive', - 'fancy', - 'veste', - 'stud' - ], - modifiable: true), - Emoji( - name: 'person in tuxedo: medium skin tone', - char: '\u{1F935}\u{1F3FD}', - shortName: 'person_in_tuxedo_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'person', - 'medium skin tone', - 'tuxedo', - 'uc9', - 'diversity', - 'wedding', - 'men', - 'boys night', - 'donald trump', - 'fame', - 'vampire', - 'las vegas', - 'rich', - 'jacket', - 'handsome', - 'costume', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'trump', - 'famous', - 'celebrity', - 'dracula', - 'vegas', - 'grand', - 'expensive', - 'fancy', - 'veste', - 'stud' - ], - modifiable: true), - Emoji( - name: 'person in tuxedo: medium-dark skin tone', - char: '\u{1F935}\u{1F3FE}', - shortName: 'person_in_tuxedo_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'person', - 'medium-dark skin tone', - 'tuxedo', - 'uc9', - 'diversity', - 'wedding', - 'men', - 'boys night', - 'donald trump', - 'fame', - 'vampire', - 'las vegas', - 'rich', - 'jacket', - 'handsome', - 'costume', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'trump', - 'famous', - 'celebrity', - 'dracula', - 'vegas', - 'grand', - 'expensive', - 'fancy', - 'veste', - 'stud' - ], - modifiable: true), - Emoji( - name: 'person in tuxedo: dark skin tone', - char: '\u{1F935}\u{1F3FF}', - shortName: 'person_in_tuxedo_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'person', - 'tuxedo', - 'uc9', - 'diversity', - 'wedding', - 'men', - 'boys night', - 'donald trump', - 'fame', - 'vampire', - 'las vegas', - 'rich', - 'jacket', - 'handsome', - 'costume', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'trump', - 'famous', - 'celebrity', - 'dracula', - 'vegas', - 'grand', - 'expensive', - 'fancy', - 'veste', - 'stud' - ], - modifiable: true), - Emoji( - name: 'woman in tuxedo', - char: '\u{1F935}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_in_tuxedo', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'wedding', - 'fame', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'famous', - 'celebrity' - ]), - Emoji( - name: 'woman in tuxedo: light skin tone', - char: '\u{1F935}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_in_tuxedo_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'wedding', - 'fame', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'woman in tuxedo: medium-light skin tone', - char: '\u{1F935}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_in_tuxedo_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'wedding', - 'fame', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'woman in tuxedo: medium skin tone', - char: '\u{1F935}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_in_tuxedo_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'wedding', - 'fame', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'woman in tuxedo: medium-dark skin tone', - char: '\u{1F935}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_in_tuxedo_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'wedding', - 'fame', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'woman in tuxedo: dark skin tone', - char: '\u{1F935}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_in_tuxedo_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'wedding', - 'fame', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'man in tuxedo', - char: '\u{1F935}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_in_tuxedo', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'wedding', - 'men', - 'boys night', - 'fame', - 'jacket', - 'handsome', - 'costume', - 'husband', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'famous', - 'celebrity', - 'veste', - 'stud' - ]), - Emoji( - name: 'man in tuxedo: light skin tone', - char: '\u{1F935}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_in_tuxedo_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'wedding', - 'men', - 'boys night', - 'fame', - 'jacket', - 'handsome', - 'costume', - 'husband', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'famous', - 'celebrity', - 'veste', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man in tuxedo: medium-light skin tone', - char: '\u{1F935}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_in_tuxedo_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'wedding', - 'men', - 'boys night', - 'fame', - 'jacket', - 'handsome', - 'costume', - 'husband', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'famous', - 'celebrity', - 'veste', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man in tuxedo: medium skin tone', - char: '\u{1F935}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_in_tuxedo_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'wedding', - 'men', - 'boys night', - 'fame', - 'jacket', - 'handsome', - 'costume', - 'husband', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'famous', - 'celebrity', - 'veste', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man in tuxedo: medium-dark skin tone', - char: '\u{1F935}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_in_tuxedo_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'wedding', - 'men', - 'boys night', - 'fame', - 'jacket', - 'handsome', - 'costume', - 'husband', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'famous', - 'celebrity', - 'veste', - 'stud' - ], - modifiable: true), - Emoji( - name: 'man in tuxedo: dark skin tone', - char: '\u{1F935}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_in_tuxedo_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'wedding', - 'men', - 'boys night', - 'fame', - 'jacket', - 'handsome', - 'costume', - 'husband', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'famous', - 'celebrity', - 'veste', - 'stud' - ], - modifiable: true), - Emoji( - name: 'princess', - char: '\u{1F478}', - shortName: 'princess', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'fairy tale', - 'fantasy', - 'uc6', - 'diversity', - 'wedding', - 'women', - 'halloween', - 'beautiful', - 'girls night', - 'power', - 'queen', - 'disney', - 'bling', - 'fame', - 'crown', - 'rich', - 'dress', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'woman', - 'female', - 'samhain', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'king', - 'prince', - 'princess', - 'cartoon', - 'jewels', - 'gems', - 'jewel', - 'jewelry', - 'treasure', - 'famous', - 'celebrity', - 'tiara', - 'grand', - 'expensive', - 'fancy' - ]), - Emoji( - name: 'princess: light skin tone', - char: '\u{1F478}\u{1F3FB}', - shortName: 'princess_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'fairy tale', - 'fantasy', - 'light skin tone', - 'uc8', - 'diversity', - 'wedding', - 'women', - 'halloween', - 'beautiful', - 'girls night', - 'power', - 'queen', - 'disney', - 'bling', - 'fame', - 'crown', - 'rich', - 'dress', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'woman', - 'female', - 'samhain', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'king', - 'prince', - 'princess', - 'cartoon', - 'jewels', - 'gems', - 'jewel', - 'jewelry', - 'treasure', - 'famous', - 'celebrity', - 'tiara', - 'grand', - 'expensive', - 'fancy' - ], - modifiable: true), - Emoji( - name: 'princess: medium-light skin tone', - char: '\u{1F478}\u{1F3FC}', - shortName: 'princess_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'fairy tale', - 'fantasy', - 'medium-light skin tone', - 'uc8', - 'diversity', - 'wedding', - 'women', - 'halloween', - 'beautiful', - 'girls night', - 'power', - 'queen', - 'disney', - 'bling', - 'fame', - 'crown', - 'rich', - 'dress', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'woman', - 'female', - 'samhain', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'king', - 'prince', - 'princess', - 'cartoon', - 'jewels', - 'gems', - 'jewel', - 'jewelry', - 'treasure', - 'famous', - 'celebrity', - 'tiara', - 'grand', - 'expensive', - 'fancy' - ], - modifiable: true), - Emoji( - name: 'princess: medium skin tone', - char: '\u{1F478}\u{1F3FD}', - shortName: 'princess_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'fairy tale', - 'fantasy', - 'medium skin tone', - 'uc8', - 'diversity', - 'wedding', - 'women', - 'halloween', - 'beautiful', - 'girls night', - 'power', - 'queen', - 'disney', - 'bling', - 'fame', - 'crown', - 'rich', - 'dress', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'woman', - 'female', - 'samhain', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'king', - 'prince', - 'princess', - 'cartoon', - 'jewels', - 'gems', - 'jewel', - 'jewelry', - 'treasure', - 'famous', - 'celebrity', - 'tiara', - 'grand', - 'expensive', - 'fancy' - ], - modifiable: true), - Emoji( - name: 'princess: medium-dark skin tone', - char: '\u{1F478}\u{1F3FE}', - shortName: 'princess_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'fairy tale', - 'fantasy', - 'medium-dark skin tone', - 'uc8', - 'diversity', - 'wedding', - 'women', - 'halloween', - 'beautiful', - 'girls night', - 'power', - 'queen', - 'disney', - 'bling', - 'fame', - 'crown', - 'rich', - 'dress', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'woman', - 'female', - 'samhain', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'king', - 'prince', - 'princess', - 'cartoon', - 'jewels', - 'gems', - 'jewel', - 'jewelry', - 'treasure', - 'famous', - 'celebrity', - 'tiara', - 'grand', - 'expensive', - 'fancy' - ], - modifiable: true), - Emoji( - name: 'princess: dark skin tone', - char: '\u{1F478}\u{1F3FF}', - shortName: 'princess_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'fairy tale', - 'fantasy', - 'uc8', - 'diversity', - 'wedding', - 'women', - 'halloween', - 'beautiful', - 'girls night', - 'power', - 'queen', - 'disney', - 'bling', - 'fame', - 'crown', - 'rich', - 'dress', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'woman', - 'female', - 'samhain', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'king', - 'prince', - 'princess', - 'cartoon', - 'jewels', - 'gems', - 'jewel', - 'jewelry', - 'treasure', - 'famous', - 'celebrity', - 'tiara', - 'grand', - 'expensive', - 'fancy' - ], - modifiable: true), - Emoji( - name: 'prince', - char: '\u{1F934}', - shortName: 'prince', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'prince', - 'uc9', - 'diversity', - 'men', - 'power', - 'queen', - 'disney', - 'bling', - 'fame', - 'crown', - 'rich', - 'handsome', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'king', - 'prince', - 'princess', - 'cartoon', - 'jewels', - 'gems', - 'jewel', - 'jewelry', - 'treasure', - 'famous', - 'celebrity', - 'tiara', - 'grand', - 'expensive', - 'fancy', - 'stud' - ]), - Emoji( - name: 'prince: light skin tone', - char: '\u{1F934}\u{1F3FB}', - shortName: 'prince_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'light skin tone', - 'prince', - 'uc9', - 'diversity', - 'men', - 'power', - 'queen', - 'disney', - 'bling', - 'fame', - 'crown', - 'rich', - 'handsome', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'king', - 'prince', - 'princess', - 'cartoon', - 'jewels', - 'gems', - 'jewel', - 'jewelry', - 'treasure', - 'famous', - 'celebrity', - 'tiara', - 'grand', - 'expensive', - 'fancy', - 'stud' - ], - modifiable: true), - Emoji( - name: 'prince: medium-light skin tone', - char: '\u{1F934}\u{1F3FC}', - shortName: 'prince_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'medium-light skin tone', - 'prince', - 'uc9', - 'diversity', - 'men', - 'power', - 'queen', - 'disney', - 'bling', - 'fame', - 'crown', - 'rich', - 'handsome', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'king', - 'prince', - 'princess', - 'cartoon', - 'jewels', - 'gems', - 'jewel', - 'jewelry', - 'treasure', - 'famous', - 'celebrity', - 'tiara', - 'grand', - 'expensive', - 'fancy', - 'stud' - ], - modifiable: true), - Emoji( - name: 'prince: medium skin tone', - char: '\u{1F934}\u{1F3FD}', - shortName: 'prince_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'medium skin tone', - 'prince', - 'uc9', - 'diversity', - 'men', - 'power', - 'queen', - 'disney', - 'bling', - 'fame', - 'crown', - 'rich', - 'handsome', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'king', - 'prince', - 'princess', - 'cartoon', - 'jewels', - 'gems', - 'jewel', - 'jewelry', - 'treasure', - 'famous', - 'celebrity', - 'tiara', - 'grand', - 'expensive', - 'fancy', - 'stud' - ], - modifiable: true), - Emoji( - name: 'prince: medium-dark skin tone', - char: '\u{1F934}\u{1F3FE}', - shortName: 'prince_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'medium-dark skin tone', - 'prince', - 'uc9', - 'diversity', - 'men', - 'power', - 'queen', - 'disney', - 'bling', - 'fame', - 'crown', - 'rich', - 'handsome', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'king', - 'prince', - 'princess', - 'cartoon', - 'jewels', - 'gems', - 'jewel', - 'jewelry', - 'treasure', - 'famous', - 'celebrity', - 'tiara', - 'grand', - 'expensive', - 'fancy', - 'stud' - ], - modifiable: true), - Emoji( - name: 'prince: dark skin tone', - char: '\u{1F934}\u{1F3FF}', - shortName: 'prince_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'prince', - 'uc9', - 'diversity', - 'men', - 'power', - 'queen', - 'disney', - 'bling', - 'fame', - 'crown', - 'rich', - 'handsome', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'king', - 'prince', - 'princess', - 'cartoon', - 'jewels', - 'gems', - 'jewel', - 'jewelry', - 'treasure', - 'famous', - 'celebrity', - 'tiara', - 'grand', - 'expensive', - 'fancy', - 'stud' - ], - modifiable: true), - Emoji( - name: 'superhero', - char: '\u{1F9B8}', - shortName: 'superhero', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'peace', - 'halloween', - 'men', - '911', - 'power', - 'daddy', - 'fame', - 'help', - 'super hero', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'peace out', - 'peace sign', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'emergency', - 'injury', - 'dad', - 'papa', - 'pere', - 'father', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman' - ]), - Emoji( - name: 'superhero: light skin tone', - char: '\u{1F9B8}\u{1F3FB}', - shortName: 'superhero_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'peace', - 'halloween', - 'men', - '911', - 'power', - 'daddy', - 'fame', - 'help', - 'super hero', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'peace out', - 'peace sign', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'emergency', - 'injury', - 'dad', - 'papa', - 'pere', - 'father', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman' - ], - modifiable: true), - Emoji( - name: 'superhero: medium-light skin tone', - char: '\u{1F9B8}\u{1F3FC}', - shortName: 'superhero_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'peace', - 'halloween', - 'men', - '911', - 'power', - 'daddy', - 'fame', - 'help', - 'super hero', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'peace out', - 'peace sign', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'emergency', - 'injury', - 'dad', - 'papa', - 'pere', - 'father', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman' - ], - modifiable: true), - Emoji( - name: 'superhero: medium skin tone', - char: '\u{1F9B8}\u{1F3FD}', - shortName: 'superhero_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'peace', - 'halloween', - 'men', - '911', - 'power', - 'daddy', - 'fame', - 'help', - 'super hero', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'peace out', - 'peace sign', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'emergency', - 'injury', - 'dad', - 'papa', - 'pere', - 'father', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman' - ], - modifiable: true), - Emoji( - name: 'superhero: medium-dark skin tone', - char: '\u{1F9B8}\u{1F3FE}', - shortName: 'superhero_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'peace', - 'halloween', - 'men', - '911', - 'power', - 'daddy', - 'fame', - 'help', - 'super hero', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'peace out', - 'peace sign', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'emergency', - 'injury', - 'dad', - 'papa', - 'pere', - 'father', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman' - ], - modifiable: true), - Emoji( - name: 'superhero: dark skin tone', - char: '\u{1F9B8}\u{1F3FF}', - shortName: 'superhero_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'peace', - 'halloween', - 'men', - '911', - 'power', - 'daddy', - 'fame', - 'help', - 'super hero', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'peace out', - 'peace sign', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'emergency', - 'injury', - 'dad', - 'papa', - 'pere', - 'father', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman' - ], - modifiable: true), - Emoji( - name: 'woman superhero', - char: '\u{1F9B8}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_superhero', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'peace', - 'halloween', - '911', - 'power', - 'fame', - 'help', - 'super hero', - 'costume', - 'mask', - 'fantasy', - 'mom', - 'proud', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'peace out', - 'peace sign', - 'samhain', - 'emergency', - 'injury', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'woman superhero: light skin tone', - char: '\u{1F9B8}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_superhero_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'peace', - 'halloween', - '911', - 'power', - 'fame', - 'help', - 'super hero', - 'costume', - 'mask', - 'fantasy', - 'mom', - 'proud', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'peace out', - 'peace sign', - 'samhain', - 'emergency', - 'injury', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman superhero: medium-light skin tone', - char: '\u{1F9B8}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_superhero_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'peace', - 'halloween', - '911', - 'power', - 'fame', - 'help', - 'super hero', - 'costume', - 'mask', - 'fantasy', - 'mom', - 'proud', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'peace out', - 'peace sign', - 'samhain', - 'emergency', - 'injury', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman superhero: medium skin tone', - char: '\u{1F9B8}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_superhero_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'peace', - 'halloween', - '911', - 'power', - 'fame', - 'help', - 'super hero', - 'costume', - 'mask', - 'fantasy', - 'mom', - 'proud', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'peace out', - 'peace sign', - 'samhain', - 'emergency', - 'injury', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman superhero: medium-dark skin tone', - char: '\u{1F9B8}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_superhero_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'peace', - 'halloween', - '911', - 'power', - 'fame', - 'help', - 'super hero', - 'costume', - 'mask', - 'fantasy', - 'mom', - 'proud', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'peace out', - 'peace sign', - 'samhain', - 'emergency', - 'injury', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman superhero: dark skin tone', - char: '\u{1F9B8}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_superhero_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'peace', - 'halloween', - '911', - 'power', - 'fame', - 'help', - 'super hero', - 'costume', - 'mask', - 'fantasy', - 'mom', - 'proud', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'peace out', - 'peace sign', - 'samhain', - 'emergency', - 'injury', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'man superhero', - char: '\u{1F9B8}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_superhero', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'peace', - 'halloween', - 'men', - '911', - 'power', - 'daddy', - 'fame', - 'help', - 'super hero', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'peace out', - 'peace sign', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'emergency', - 'injury', - 'dad', - 'papa', - 'pere', - 'father', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman' - ]), - Emoji( - name: 'man superhero: light skin tone', - char: '\u{1F9B8}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_superhero_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'peace', - 'halloween', - 'men', - '911', - 'power', - 'daddy', - 'fame', - 'help', - 'super hero', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'peace out', - 'peace sign', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'emergency', - 'injury', - 'dad', - 'papa', - 'pere', - 'father', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman' - ], - modifiable: true), - Emoji( - name: 'man superhero: medium-light skin tone', - char: '\u{1F9B8}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_superhero_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'peace', - 'halloween', - 'men', - '911', - 'power', - 'daddy', - 'fame', - 'help', - 'super hero', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'peace out', - 'peace sign', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'emergency', - 'injury', - 'dad', - 'papa', - 'pere', - 'father', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman' - ], - modifiable: true), - Emoji( - name: 'man superhero: medium skin tone', - char: '\u{1F9B8}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_superhero_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'peace', - 'halloween', - 'men', - '911', - 'power', - 'daddy', - 'fame', - 'help', - 'super hero', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'peace out', - 'peace sign', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'emergency', - 'injury', - 'dad', - 'papa', - 'pere', - 'father', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman' - ], - modifiable: true), - Emoji( - name: 'man superhero: medium-dark skin tone', - char: '\u{1F9B8}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_superhero_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'peace', - 'halloween', - 'men', - '911', - 'power', - 'daddy', - 'fame', - 'help', - 'super hero', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'peace out', - 'peace sign', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'emergency', - 'injury', - 'dad', - 'papa', - 'pere', - 'father', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman' - ], - modifiable: true), - Emoji( - name: 'man superhero: dark skin tone', - char: '\u{1F9B8}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_superhero_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'peace', - 'halloween', - 'men', - '911', - 'power', - 'daddy', - 'fame', - 'help', - 'super hero', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'peace out', - 'peace sign', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'emergency', - 'injury', - 'dad', - 'papa', - 'pere', - 'father', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman' - ], - modifiable: true), - Emoji( - name: 'supervillain', - char: '\u{1F9B9}', - shortName: 'supervillain', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'halloween', - 'men', - 'power', - 'evil', - 'fame', - 'guilty', - 'super hero', - 'killer', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'greed', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman', - 'savage', - 'scary clown', - 'selfish' - ]), - Emoji( - name: 'supervillain: light skin tone', - char: '\u{1F9B9}\u{1F3FB}', - shortName: 'supervillain_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'halloween', - 'men', - 'power', - 'evil', - 'fame', - 'guilty', - 'super hero', - 'killer', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'greed', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman', - 'savage', - 'scary clown', - 'selfish' - ], - modifiable: true), - Emoji( - name: 'supervillain: medium-light skin tone', - char: '\u{1F9B9}\u{1F3FC}', - shortName: 'supervillain_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'halloween', - 'men', - 'power', - 'evil', - 'fame', - 'guilty', - 'super hero', - 'killer', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'greed', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman', - 'savage', - 'scary clown', - 'selfish' - ], - modifiable: true), - Emoji( - name: 'supervillain: medium skin tone', - char: '\u{1F9B9}\u{1F3FD}', - shortName: 'supervillain_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'halloween', - 'men', - 'power', - 'evil', - 'fame', - 'guilty', - 'super hero', - 'killer', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'greed', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman', - 'savage', - 'scary clown', - 'selfish' - ], - modifiable: true), - Emoji( - name: 'supervillain: medium-dark skin tone', - char: '\u{1F9B9}\u{1F3FE}', - shortName: 'supervillain_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'halloween', - 'men', - 'power', - 'evil', - 'fame', - 'guilty', - 'super hero', - 'killer', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'greed', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman', - 'savage', - 'scary clown', - 'selfish' - ], - modifiable: true), - Emoji( - name: 'supervillain: dark skin tone', - char: '\u{1F9B9}\u{1F3FF}', - shortName: 'supervillain_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'halloween', - 'men', - 'power', - 'evil', - 'fame', - 'guilty', - 'super hero', - 'killer', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'greed', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman', - 'savage', - 'scary clown', - 'selfish' - ], - modifiable: true), - Emoji( - name: 'woman supervillain: light skin tone', - char: '\u{1F9B9}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_supervillain_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'halloween', - 'power', - 'evil', - 'fame', - 'guilty', - 'super hero', - 'killer', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'greed', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman', - 'savage', - 'scary clown', - 'selfish' - ], - modifiable: true), - Emoji( - name: 'woman supervillain', - char: '\u{1F9B9}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_supervillain', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'halloween', - 'power', - 'evil', - 'fame', - 'guilty', - 'super hero', - 'killer', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'greed', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman', - 'savage', - 'scary clown', - 'selfish' - ]), - Emoji( - name: 'woman supervillain: medium-light skin tone', - char: '\u{1F9B9}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_supervillain_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'halloween', - 'power', - 'evil', - 'fame', - 'guilty', - 'super hero', - 'killer', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'greed', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman', - 'savage', - 'scary clown', - 'selfish' - ], - modifiable: true), - Emoji( - name: 'woman supervillain: medium skin tone', - char: '\u{1F9B9}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_supervillain_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'halloween', - 'power', - 'evil', - 'fame', - 'guilty', - 'super hero', - 'killer', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'greed', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman', - 'savage', - 'scary clown', - 'selfish' - ], - modifiable: true), - Emoji( - name: 'woman supervillain: medium-dark skin tone', - char: '\u{1F9B9}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_supervillain_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'halloween', - 'power', - 'evil', - 'fame', - 'guilty', - 'super hero', - 'killer', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'greed', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman', - 'savage', - 'scary clown', - 'selfish' - ], - modifiable: true), - Emoji( - name: 'woman supervillain: dark skin tone', - char: '\u{1F9B9}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_supervillain_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'halloween', - 'power', - 'evil', - 'fame', - 'guilty', - 'super hero', - 'killer', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'greed', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman', - 'savage', - 'scary clown', - 'selfish' - ], - modifiable: true), - Emoji( - name: 'man supervillain', - char: '\u{1F9B9}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_supervillain', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'halloween', - 'men', - 'power', - 'evil', - 'fame', - 'guilty', - 'super hero', - 'killer', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'greed', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman', - 'savage', - 'scary clown', - 'selfish' - ]), - Emoji( - name: 'man supervillain: light skin tone', - char: '\u{1F9B9}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_supervillain_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'halloween', - 'men', - 'power', - 'evil', - 'fame', - 'guilty', - 'super hero', - 'killer', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'greed', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman', - 'savage', - 'scary clown', - 'selfish' - ], - modifiable: true), - Emoji( - name: 'man supervillain: medium-light skin tone', - char: '\u{1F9B9}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_supervillain_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'halloween', - 'men', - 'power', - 'evil', - 'fame', - 'guilty', - 'super hero', - 'killer', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'greed', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman', - 'savage', - 'scary clown', - 'selfish' - ], - modifiable: true), - Emoji( - name: 'man supervillain: medium skin tone', - char: '\u{1F9B9}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_supervillain_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'halloween', - 'men', - 'power', - 'evil', - 'fame', - 'guilty', - 'super hero', - 'killer', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'greed', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman', - 'savage', - 'scary clown', - 'selfish' - ], - modifiable: true), - Emoji( - name: 'man supervillain: medium-dark skin tone', - char: '\u{1F9B9}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_supervillain_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'halloween', - 'men', - 'power', - 'evil', - 'fame', - 'guilty', - 'super hero', - 'killer', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'greed', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman', - 'savage', - 'scary clown', - 'selfish' - ], - modifiable: true), - Emoji( - name: 'man supervillain: dark skin tone', - char: '\u{1F9B9}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_supervillain_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc11', - 'diversity', - 'halloween', - 'men', - 'power', - 'evil', - 'fame', - 'guilty', - 'super hero', - 'killer', - 'costume', - 'mask', - 'fantasy', - 'proud', - 'greed', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'famous', - 'celebrity', - 'superhero', - 'superman', - 'batman', - 'savage', - 'scary clown', - 'selfish' - ], - modifiable: true), - Emoji( - name: 'ninja', - char: '\u{1F977}', - shortName: 'ninja', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'japan', - 'evil', - 'chinese', - 'super hero', - 'killer', - 'mask', - 'disguise', - 'shinobi', - 'japanese', - 'ninja', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'chinois', - 'asian', - 'chine', - 'superhero', - 'superman', - 'batman', - 'savage', - 'scary clown', - 'samurai' - ]), - Emoji( - name: 'ninja: light skin tone', - char: '\u{1F977}\u{1F3FB}', - shortName: 'ninja_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'japan', - 'evil', - 'chinese', - 'super hero', - 'killer', - 'mask', - 'disguise', - 'shinobi', - 'japanese', - 'ninja', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'chinois', - 'asian', - 'chine', - 'superhero', - 'superman', - 'batman', - 'savage', - 'scary clown', - 'samurai' - ], - modifiable: true), - Emoji( - name: 'ninja: medium-light skin tone', - char: '\u{1F977}\u{1F3FC}', - shortName: 'ninja_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'japan', - 'evil', - 'chinese', - 'super hero', - 'killer', - 'mask', - 'disguise', - 'shinobi', - 'japanese', - 'ninja', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'chinois', - 'asian', - 'chine', - 'superhero', - 'superman', - 'batman', - 'savage', - 'scary clown', - 'samurai' - ], - modifiable: true), - Emoji( - name: 'ninja: medium skin tone', - char: '\u{1F977}\u{1F3FD}', - shortName: 'ninja_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'japan', - 'evil', - 'chinese', - 'super hero', - 'killer', - 'mask', - 'disguise', - 'shinobi', - 'japanese', - 'ninja', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'chinois', - 'asian', - 'chine', - 'superhero', - 'superman', - 'batman', - 'savage', - 'scary clown', - 'samurai' - ], - modifiable: true), - Emoji( - name: 'ninja: medium-dark skin tone', - char: '\u{1F977}\u{1F3FE}', - shortName: 'ninja_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'japan', - 'evil', - 'chinese', - 'super hero', - 'killer', - 'mask', - 'disguise', - 'shinobi', - 'japanese', - 'ninja', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'chinois', - 'asian', - 'chine', - 'superhero', - 'superman', - 'batman', - 'savage', - 'scary clown', - 'samurai' - ], - modifiable: true), - Emoji( - name: 'ninja: dark skin tone', - char: '\u{1F977}\u{1F3FF}', - shortName: 'ninja_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'japan', - 'evil', - 'chinese', - 'super hero', - 'killer', - 'mask', - 'disguise', - 'shinobi', - 'japanese', - 'ninja', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'chinois', - 'asian', - 'chine', - 'superhero', - 'superman', - 'batman', - 'savage', - 'scary clown', - 'samurai' - ], - modifiable: true), - Emoji( - name: 'mx claus', - char: '\u{1F9D1}\u{200D}\u{1F384}', - shortName: 'mx_claus', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc13', - 'holidays', - 'winter', - 'christmas', - 'advent', - 'fantasy', - 'disguise', - 'holiday', - 'navidad', - 'xmas', - 'noel', - 'merry christmas' - ]), - Emoji( - name: 'mx claus: light skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F384}', - shortName: 'mx_claus_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc13', - 'holidays', - 'winter', - 'christmas', - 'advent', - 'fantasy', - 'disguise', - 'holiday', - 'navidad', - 'xmas', - 'noel', - 'merry christmas' - ], - modifiable: true), - Emoji( - name: 'mx claus: medium-light skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F384}', - shortName: 'mx_claus_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc13', - 'holidays', - 'winter', - 'christmas', - 'advent', - 'fantasy', - 'disguise', - 'holiday', - 'navidad', - 'xmas', - 'noel', - 'merry christmas' - ], - modifiable: true), - Emoji( - name: 'mx claus: medium skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F384}', - shortName: 'mx_claus_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc13', - 'holidays', - 'winter', - 'christmas', - 'advent', - 'fantasy', - 'disguise', - 'holiday', - 'navidad', - 'xmas', - 'noel', - 'merry christmas' - ], - modifiable: true), - Emoji( - name: 'mx claus: medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F384}', - shortName: 'mx_claus_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc13', - 'holidays', - 'winter', - 'christmas', - 'advent', - 'fantasy', - 'disguise', - 'holiday', - 'navidad', - 'xmas', - 'noel', - 'merry christmas' - ], - modifiable: true), - Emoji( - name: 'mx claus: dark skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F384}', - shortName: 'mx_claus_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc13', - 'holidays', - 'winter', - 'christmas', - 'advent', - 'fantasy', - 'disguise', - 'holiday', - 'navidad', - 'xmas', - 'noel', - 'merry christmas' - ], - modifiable: true), - Emoji( - name: 'Mrs. Claus', - char: '\u{1F936}', - shortName: 'mrs_claus', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Christmas', - 'Mrs.', - 'celebration', - 'claus', - 'mother', - 'uc9', - 'holidays', - 'diversity', - 'winter', - 'christmas', - 'santa', - 'advent', - 'fantasy', - 'disguise', - 'holiday', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'santa clause', - 'santa claus' - ]), - Emoji( - name: 'Mrs. Claus: light skin tone', - char: '\u{1F936}\u{1F3FB}', - shortName: 'mrs_claus_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Christmas', - 'Mrs.', - 'celebration', - 'claus', - 'light skin tone', - 'mother', - 'uc9', - 'holidays', - 'diversity', - 'winter', - 'christmas', - 'santa', - 'advent', - 'fantasy', - 'disguise', - 'holiday', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'santa clause', - 'santa claus' - ], - modifiable: true), - Emoji( - name: 'Mrs. Claus: medium-light skin tone', - char: '\u{1F936}\u{1F3FC}', - shortName: 'mrs_claus_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Christmas', - 'Mrs.', - 'celebration', - 'claus', - 'medium-light skin tone', - 'mother', - 'uc9', - 'holidays', - 'diversity', - 'winter', - 'christmas', - 'santa', - 'advent', - 'fantasy', - 'disguise', - 'holiday', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'santa clause', - 'santa claus' - ], - modifiable: true), - Emoji( - name: 'Mrs. Claus: medium skin tone', - char: '\u{1F936}\u{1F3FD}', - shortName: 'mrs_claus_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Christmas', - 'Mrs.', - 'celebration', - 'claus', - 'medium skin tone', - 'mother', - 'uc9', - 'holidays', - 'diversity', - 'winter', - 'christmas', - 'santa', - 'advent', - 'fantasy', - 'disguise', - 'holiday', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'santa clause', - 'santa claus' - ], - modifiable: true), - Emoji( - name: 'Mrs. Claus: medium-dark skin tone', - char: '\u{1F936}\u{1F3FE}', - shortName: 'mrs_claus_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Christmas', - 'Mrs.', - 'celebration', - 'claus', - 'medium-dark skin tone', - 'mother', - 'uc9', - 'holidays', - 'diversity', - 'winter', - 'christmas', - 'santa', - 'advent', - 'fantasy', - 'disguise', - 'holiday', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'santa clause', - 'santa claus' - ], - modifiable: true), - Emoji( - name: 'Mrs. Claus: dark skin tone', - char: '\u{1F936}\u{1F3FF}', - shortName: 'mrs_claus_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Christmas', - 'Mrs.', - 'celebration', - 'claus', - 'dark skin tone', - 'mother', - 'uc9', - 'holidays', - 'diversity', - 'winter', - 'christmas', - 'santa', - 'advent', - 'fantasy', - 'disguise', - 'holiday', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'santa clause', - 'santa claus' - ], - modifiable: true), - Emoji( - name: 'Santa Claus', - char: '\u{1F385}', - shortName: 'santa', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Christmas', - 'celebration', - 'claus', - 'father', - 'santa', - 'uc6', - 'holidays', - 'diversity', - 'winter', - 'christmas', - 'santa', - 'mustache', - 'advent', - 'beard', - 'fantasy', - 'disguise', - 'holiday', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'santa clause', - 'santa claus' - ]), - Emoji( - name: 'Santa Claus: light skin tone', - char: '\u{1F385}\u{1F3FB}', - shortName: 'santa_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Christmas', - 'celebration', - 'claus', - 'father', - 'light skin tone', - 'santa', - 'uc8', - 'holidays', - 'diversity', - 'winter', - 'christmas', - 'santa', - 'mustache', - 'advent', - 'beard', - 'fantasy', - 'disguise', - 'holiday', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'santa clause', - 'santa claus' - ], - modifiable: true), - Emoji( - name: 'Santa Claus: medium-light skin tone', - char: '\u{1F385}\u{1F3FC}', - shortName: 'santa_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Christmas', - 'celebration', - 'claus', - 'father', - 'medium-light skin tone', - 'santa', - 'uc8', - 'holidays', - 'diversity', - 'winter', - 'christmas', - 'santa', - 'mustache', - 'advent', - 'beard', - 'fantasy', - 'disguise', - 'holiday', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'santa clause', - 'santa claus' - ], - modifiable: true), - Emoji( - name: 'Santa Claus: medium skin tone', - char: '\u{1F385}\u{1F3FD}', - shortName: 'santa_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Christmas', - 'celebration', - 'claus', - 'father', - 'medium skin tone', - 'santa', - 'uc8', - 'holidays', - 'diversity', - 'winter', - 'christmas', - 'santa', - 'mustache', - 'advent', - 'beard', - 'fantasy', - 'disguise', - 'holiday', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'santa clause', - 'santa claus' - ], - modifiable: true), - Emoji( - name: 'Santa Claus: medium-dark skin tone', - char: '\u{1F385}\u{1F3FE}', - shortName: 'santa_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Christmas', - 'celebration', - 'claus', - 'father', - 'medium-dark skin tone', - 'santa', - 'uc8', - 'holidays', - 'diversity', - 'winter', - 'christmas', - 'santa', - 'mustache', - 'advent', - 'beard', - 'fantasy', - 'disguise', - 'holiday', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'santa clause', - 'santa claus' - ], - modifiable: true), - Emoji( - name: 'Santa Claus: dark skin tone', - char: '\u{1F385}\u{1F3FF}', - shortName: 'santa_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Christmas', - 'celebration', - 'claus', - 'dark skin tone', - 'father', - 'santa', - 'uc8', - 'holidays', - 'diversity', - 'winter', - 'christmas', - 'santa', - 'mustache', - 'advent', - 'beard', - 'fantasy', - 'disguise', - 'holiday', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'santa clause', - 'santa claus' - ], - modifiable: true), - Emoji( - name: 'mage', - char: '\u{1F9D9}', - shortName: 'mage', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'sorcerer', - 'sorceress', - 'witch', - 'wizard', - 'uc10', - 'diversity', - 'halloween', - 'magic', - 'wizard', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'spell', - 'genie', - 'magical', - 'Sorcerer', - 'Sorceress', - 'witch' - ]), - Emoji( - name: 'mage: light skin tone', - char: '\u{1F9D9}\u{1F3FB}', - shortName: 'mage_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'light skin tone', - 'sorcerer', - 'sorceress', - 'witch', - 'wizard', - 'uc10', - 'diversity', - 'halloween', - 'magic', - 'wizard', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'spell', - 'genie', - 'magical', - 'Sorcerer', - 'Sorceress', - 'witch' - ], - modifiable: true), - Emoji( - name: 'mage: medium-light skin tone', - char: '\u{1F9D9}\u{1F3FC}', - shortName: 'mage_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'medium-light skin tone', - 'sorcerer', - 'sorceress', - 'witch', - 'wizard', - 'uc10', - 'diversity', - 'halloween', - 'magic', - 'wizard', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'spell', - 'genie', - 'magical', - 'Sorcerer', - 'Sorceress', - 'witch' - ], - modifiable: true), - Emoji( - name: 'mage: medium skin tone', - char: '\u{1F9D9}\u{1F3FD}', - shortName: 'mage_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'medium skin tone', - 'sorcerer', - 'sorceress', - 'witch', - 'wizard', - 'uc10', - 'diversity', - 'halloween', - 'magic', - 'wizard', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'spell', - 'genie', - 'magical', - 'Sorcerer', - 'Sorceress', - 'witch' - ], - modifiable: true), - Emoji( - name: 'mage: medium-dark skin tone', - char: '\u{1F9D9}\u{1F3FE}', - shortName: 'mage_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'medium-dark skin tone', - 'sorcerer', - 'sorceress', - 'witch', - 'wizard', - 'uc10', - 'diversity', - 'halloween', - 'magic', - 'wizard', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'spell', - 'genie', - 'magical', - 'Sorcerer', - 'Sorceress', - 'witch' - ], - modifiable: true), - Emoji( - name: 'mage: dark skin tone', - char: '\u{1F9D9}\u{1F3FF}', - shortName: 'mage_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'dark skin tone', - 'sorcerer', - 'sorceress', - 'witch', - 'wizard', - 'uc10', - 'diversity', - 'halloween', - 'magic', - 'wizard', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'spell', - 'genie', - 'magical', - 'Sorcerer', - 'Sorceress', - 'witch' - ], - modifiable: true), - Emoji( - name: 'woman mage', - char: '\u{1F9D9}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_mage', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'sorceress', - 'witch', - 'uc10', - 'diversity', - 'halloween', - 'magic', - 'wizard', - 'fantasy', - 'disguise', - 'snow white', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'spell', - 'genie', - 'magical', - 'Sorcerer', - 'Sorceress', - 'witch' - ]), - Emoji( - name: 'woman mage: light skin tone', - char: '\u{1F9D9}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_mage_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'light skin tone', - 'sorceress', - 'witch', - 'uc10', - 'diversity', - 'halloween', - 'magic', - 'wizard', - 'fantasy', - 'disguise', - 'snow white', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'spell', - 'genie', - 'magical', - 'Sorcerer', - 'Sorceress', - 'witch' - ], - modifiable: true), - Emoji( - name: 'woman mage: medium-light skin tone', - char: '\u{1F9D9}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_mage_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'medium-light skin tone', - 'sorceress', - 'witch', - 'uc10', - 'diversity', - 'halloween', - 'magic', - 'wizard', - 'fantasy', - 'disguise', - 'snow white', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'spell', - 'genie', - 'magical', - 'Sorcerer', - 'Sorceress', - 'witch' - ], - modifiable: true), - Emoji( - name: 'woman mage: medium skin tone', - char: '\u{1F9D9}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_mage_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'medium skin tone', - 'sorceress', - 'witch', - 'uc10', - 'diversity', - 'halloween', - 'magic', - 'wizard', - 'fantasy', - 'disguise', - 'snow white', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'spell', - 'genie', - 'magical', - 'Sorcerer', - 'Sorceress', - 'witch' - ], - modifiable: true), - Emoji( - name: 'woman mage: medium-dark skin tone', - char: '\u{1F9D9}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_mage_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'medium-dark skin tone', - 'sorceress', - 'witch', - 'uc10', - 'diversity', - 'halloween', - 'magic', - 'wizard', - 'fantasy', - 'disguise', - 'snow white', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'spell', - 'genie', - 'magical', - 'Sorcerer', - 'Sorceress', - 'witch' - ], - modifiable: true), - Emoji( - name: 'woman mage: dark skin tone', - char: '\u{1F9D9}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_mage_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'dark skin tone', - 'sorceress', - 'witch', - 'uc10', - 'diversity', - 'halloween', - 'magic', - 'wizard', - 'fantasy', - 'disguise', - 'snow white', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'spell', - 'genie', - 'magical', - 'Sorcerer', - 'Sorceress', - 'witch' - ], - modifiable: true), - Emoji( - name: 'man mage', - char: '\u{1F9D9}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_mage', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'sorcerer', - 'wizard', - 'uc10', - 'diversity', - 'halloween', - 'magic', - 'beard', - 'wizard', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'spell', - 'genie', - 'magical', - 'Sorcerer', - 'Sorceress', - 'witch' - ]), - Emoji( - name: 'man mage: light skin tone', - char: '\u{1F9D9}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_mage_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'light skin tone', - 'sorcerer', - 'wizard', - 'uc10', - 'diversity', - 'halloween', - 'magic', - 'beard', - 'wizard', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'spell', - 'genie', - 'magical', - 'Sorcerer', - 'Sorceress', - 'witch' - ], - modifiable: true), - Emoji( - name: 'man mage: medium-light skin tone', - char: '\u{1F9D9}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_mage_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'medium-light skin tone', - 'sorcerer', - 'wizard', - 'uc10', - 'diversity', - 'halloween', - 'magic', - 'beard', - 'wizard', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'spell', - 'genie', - 'magical', - 'Sorcerer', - 'Sorceress', - 'witch' - ], - modifiable: true), - Emoji( - name: 'man mage: medium skin tone', - char: '\u{1F9D9}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_mage_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'medium skin tone', - 'sorcerer', - 'wizard', - 'uc10', - 'diversity', - 'halloween', - 'magic', - 'beard', - 'wizard', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'spell', - 'genie', - 'magical', - 'Sorcerer', - 'Sorceress', - 'witch' - ], - modifiable: true), - Emoji( - name: 'man mage: medium-dark skin tone', - char: '\u{1F9D9}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_mage_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'medium-dark skin tone', - 'sorcerer', - 'wizard', - 'uc10', - 'diversity', - 'halloween', - 'magic', - 'beard', - 'wizard', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'spell', - 'genie', - 'magical', - 'Sorcerer', - 'Sorceress', - 'witch' - ], - modifiable: true), - Emoji( - name: 'man mage: dark skin tone', - char: '\u{1F9D9}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_mage_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'dark skin tone', - 'sorcerer', - 'wizard', - 'uc10', - 'diversity', - 'halloween', - 'magic', - 'beard', - 'wizard', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'spell', - 'genie', - 'magical', - 'Sorcerer', - 'Sorceress', - 'witch' - ], - modifiable: true), - Emoji( - name: 'elf', - char: '\u{1F9DD}', - shortName: 'elf', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc10', - 'diversity', - 'halloween', - 'christmas', - 'magic', - 'legolas', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'spell', - 'genie', - 'magical' - ]), - Emoji( - name: 'elf: light skin tone', - char: '\u{1F9DD}\u{1F3FB}', - shortName: 'elf_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'light skin tone', - 'magical', - 'uc10', - 'diversity', - 'halloween', - 'christmas', - 'magic', - 'legolas', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'spell', - 'genie', - 'magical' - ], - modifiable: true), - Emoji( - name: 'elf: medium-light skin tone', - char: '\u{1F9DD}\u{1F3FC}', - shortName: 'elf_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'magical', - 'medium-light skin tone', - 'uc10', - 'diversity', - 'halloween', - 'christmas', - 'magic', - 'legolas', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'spell', - 'genie', - 'magical' - ], - modifiable: true), - Emoji( - name: 'elf: medium skin tone', - char: '\u{1F9DD}\u{1F3FD}', - shortName: 'elf_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'magical', - 'medium skin tone', - 'uc10', - 'diversity', - 'halloween', - 'christmas', - 'magic', - 'legolas', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'spell', - 'genie', - 'magical' - ], - modifiable: true), - Emoji( - name: 'elf: medium-dark skin tone', - char: '\u{1F9DD}\u{1F3FE}', - shortName: 'elf_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'magical', - 'medium-dark skin tone', - 'uc10', - 'diversity', - 'halloween', - 'christmas', - 'magic', - 'legolas', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'spell', - 'genie', - 'magical' - ], - modifiable: true), - Emoji( - name: 'elf: dark skin tone', - char: '\u{1F9DD}\u{1F3FF}', - shortName: 'elf_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'dark skin tone', - 'magical', - 'uc10', - 'diversity', - 'halloween', - 'christmas', - 'magic', - 'legolas', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'spell', - 'genie', - 'magical' - ], - modifiable: true), - Emoji( - name: 'woman elf', - char: '\u{1F9DD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_elf', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'magical', - 'uc10', - 'diversity', - 'halloween', - 'christmas', - 'magic', - 'legolas', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'spell', - 'genie', - 'magical' - ]), - Emoji( - name: 'woman elf: light skin tone', - char: '\u{1F9DD}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_elf_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'light skin tone', - 'magical', - 'uc10', - 'diversity', - 'halloween', - 'christmas', - 'magic', - 'legolas', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'spell', - 'genie', - 'magical' - ], - modifiable: true), - Emoji( - name: 'woman elf: medium-light skin tone', - char: '\u{1F9DD}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_elf_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'magical', - 'medium-light skin tone', - 'uc10', - 'diversity', - 'halloween', - 'christmas', - 'magic', - 'legolas', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'spell', - 'genie', - 'magical' - ], - modifiable: true), - Emoji( - name: 'woman elf: medium skin tone', - char: '\u{1F9DD}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_elf_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'magical', - 'medium skin tone', - 'uc10', - 'diversity', - 'halloween', - 'christmas', - 'magic', - 'legolas', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'spell', - 'genie', - 'magical' - ], - modifiable: true), - Emoji( - name: 'woman elf: medium-dark skin tone', - char: '\u{1F9DD}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_elf_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'magical', - 'medium-dark skin tone', - 'uc10', - 'diversity', - 'halloween', - 'christmas', - 'magic', - 'legolas', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'spell', - 'genie', - 'magical' - ], - modifiable: true), - Emoji( - name: 'woman elf: dark skin tone', - char: '\u{1F9DD}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_elf_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'dark skin tone', - 'magical', - 'uc10', - 'diversity', - 'halloween', - 'christmas', - 'magic', - 'legolas', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'spell', - 'genie', - 'magical' - ], - modifiable: true), - Emoji( - name: 'man elf', - char: '\u{1F9DD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_elf', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'magical', - 'uc10', - 'diversity', - 'halloween', - 'christmas', - 'magic', - 'legolas', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'spell', - 'genie', - 'magical' - ]), - Emoji( - name: 'man elf: light skin tone', - char: '\u{1F9DD}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_elf_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'light skin tone', - 'magical', - 'uc10', - 'diversity', - 'halloween', - 'christmas', - 'magic', - 'legolas', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'spell', - 'genie', - 'magical' - ], - modifiable: true), - Emoji( - name: 'man elf: medium-light skin tone', - char: '\u{1F9DD}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_elf_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'magical', - 'medium-light skin tone', - 'uc10', - 'diversity', - 'halloween', - 'christmas', - 'magic', - 'legolas', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'spell', - 'genie', - 'magical' - ], - modifiable: true), - Emoji( - name: 'man elf: medium skin tone', - char: '\u{1F9DD}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_elf_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'magical', - 'medium skin tone', - 'uc10', - 'diversity', - 'halloween', - 'christmas', - 'magic', - 'legolas', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'spell', - 'genie', - 'magical' - ], - modifiable: true), - Emoji( - name: 'man elf: medium-dark skin tone', - char: '\u{1F9DD}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_elf_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'magical', - 'medium-dark skin tone', - 'uc10', - 'diversity', - 'halloween', - 'christmas', - 'magic', - 'legolas', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'spell', - 'genie', - 'magical' - ], - modifiable: true), - Emoji( - name: 'man elf: dark skin tone', - char: '\u{1F9DD}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_elf_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'dark skin tone', - 'magical', - 'uc10', - 'diversity', - 'halloween', - 'christmas', - 'magic', - 'legolas', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'spell', - 'genie', - 'magical' - ], - modifiable: true), - Emoji( - name: 'vampire', - char: '\u{1F9DB}', - shortName: 'vampire', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Dracula', - 'undead', - 'uc10', - 'diversity', - 'halloween', - 'vampire', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'dracula' - ]), - Emoji( - name: 'vampire: light skin tone', - char: '\u{1F9DB}\u{1F3FB}', - shortName: 'vampire_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Dracula', - 'light skin tone', - 'undead', - 'uc10', - 'diversity', - 'halloween', - 'vampire', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'dracula' - ], - modifiable: true), - Emoji( - name: 'vampire: medium-light skin tone', - char: '\u{1F9DB}\u{1F3FC}', - shortName: 'vampire_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Dracula', - 'medium-light skin tone', - 'undead', - 'uc10', - 'diversity', - 'halloween', - 'vampire', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'dracula' - ], - modifiable: true), - Emoji( - name: 'vampire: medium skin tone', - char: '\u{1F9DB}\u{1F3FD}', - shortName: 'vampire_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Dracula', - 'medium skin tone', - 'undead', - 'uc10', - 'diversity', - 'halloween', - 'vampire', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'dracula' - ], - modifiable: true), - Emoji( - name: 'vampire: medium-dark skin tone', - char: '\u{1F9DB}\u{1F3FE}', - shortName: 'vampire_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Dracula', - 'medium-dark skin tone', - 'undead', - 'uc10', - 'diversity', - 'halloween', - 'vampire', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'dracula' - ], - modifiable: true), - Emoji( - name: 'vampire: dark skin tone', - char: '\u{1F9DB}\u{1F3FF}', - shortName: 'vampire_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Dracula', - 'dark skin tone', - 'undead', - 'uc10', - 'diversity', - 'halloween', - 'vampire', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'dracula' - ], - modifiable: true), - Emoji( - name: 'woman vampire', - char: '\u{1F9DB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_vampire', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'undead', - 'uc10', - 'diversity', - 'halloween', - 'vampire', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'dracula' - ]), - Emoji( - name: 'woman vampire: light skin tone', - char: '\u{1F9DB}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_vampire_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'light skin tone', - 'undead', - 'uc10', - 'diversity', - 'halloween', - 'vampire', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'dracula' - ], - modifiable: true), - Emoji( - name: 'woman vampire: medium-light skin tone', - char: '\u{1F9DB}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_vampire_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'medium-light skin tone', - 'undead', - 'uc10', - 'diversity', - 'halloween', - 'vampire', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'dracula' - ], - modifiable: true), - Emoji( - name: 'woman vampire: medium skin tone', - char: '\u{1F9DB}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_vampire_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'medium skin tone', - 'undead', - 'uc10', - 'diversity', - 'halloween', - 'vampire', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'dracula' - ], - modifiable: true), - Emoji( - name: 'woman vampire: medium-dark skin tone', - char: '\u{1F9DB}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_vampire_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'medium-dark skin tone', - 'undead', - 'uc10', - 'diversity', - 'halloween', - 'vampire', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'dracula' - ], - modifiable: true), - Emoji( - name: 'woman vampire: dark skin tone', - char: '\u{1F9DB}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_vampire_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'dark skin tone', - 'undead', - 'uc10', - 'diversity', - 'halloween', - 'vampire', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'dracula' - ], - modifiable: true), - Emoji( - name: 'man vampire', - char: '\u{1F9DB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_vampire', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Dracula', - 'undead', - 'uc10', - 'diversity', - 'halloween', - 'vampire', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'dracula' - ]), - Emoji( - name: 'man vampire: light skin tone', - char: '\u{1F9DB}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_vampire_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Dracula', - 'light skin tone', - 'undead', - 'uc10', - 'diversity', - 'halloween', - 'vampire', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'dracula' - ], - modifiable: true), - Emoji( - name: 'man vampire: medium-light skin tone', - char: '\u{1F9DB}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_vampire_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Dracula', - 'medium-light skin tone', - 'undead', - 'uc10', - 'diversity', - 'halloween', - 'vampire', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'dracula' - ], - modifiable: true), - Emoji( - name: 'man vampire: medium skin tone', - char: '\u{1F9DB}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_vampire_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Dracula', - 'medium skin tone', - 'undead', - 'uc10', - 'diversity', - 'halloween', - 'vampire', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'dracula' - ], - modifiable: true), - Emoji( - name: 'man vampire: medium-dark skin tone', - char: '\u{1F9DB}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_vampire_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Dracula', - 'medium-dark skin tone', - 'undead', - 'uc10', - 'diversity', - 'halloween', - 'vampire', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'dracula' - ], - modifiable: true), - Emoji( - name: 'man vampire: dark skin tone', - char: '\u{1F9DB}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_vampire_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Dracula', - 'dark skin tone', - 'undead', - 'uc10', - 'diversity', - 'halloween', - 'vampire', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'dracula' - ], - modifiable: true), - Emoji( - name: 'zombie', - char: '\u{1F9DF}', - shortName: 'zombie', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc10', - 'halloween', - 'monster', - 'disguise', - 'samhain', - 'monsters', - 'beast' - ]), - Emoji( - name: 'woman zombie', - char: '\u{1F9DF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_zombie', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'undead', - 'walking dead', - 'uc10', - 'halloween', - 'monster', - 'fantasy', - 'disguise', - 'samhain', - 'monsters', - 'beast' - ]), - Emoji( - name: 'man zombie', - char: '\u{1F9DF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_zombie', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'undead', - 'walking dead', - 'uc10', - 'halloween', - 'monster', - 'fantasy', - 'disguise', - 'samhain', - 'monsters', - 'beast' - ]), - Emoji( - name: 'genie', - char: '\u{1F9DE}', - shortName: 'genie', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'uc10', - 'halloween', - 'magic', - 'djinni', - 'fantasy', - 'disguise', - 'samhain', - 'spell', - 'genie', - 'magical', - 'jinni' - ]), - Emoji( - name: 'woman genie', - char: '\u{1F9DE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_genie', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'djinn', - 'uc10', - 'halloween', - 'magic', - 'djinni', - 'fantasy', - 'disguise', - 'samhain', - 'spell', - 'genie', - 'magical', - 'jinni' - ]), - Emoji( - name: 'man genie', - char: '\u{1F9DE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_genie', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'djinn', - 'uc10', - 'halloween', - 'magic', - 'disney', - 'djinni', - 'fantasy', - 'disguise', - 'samhain', - 'spell', - 'genie', - 'magical', - 'cartoon', - 'jinni' - ]), - Emoji( - name: 'merperson', - char: '\u{1F9DC}', - shortName: 'merperson', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'mermaid', - 'merman', - 'merwoman', - 'uc10', - 'diversity', - 'halloween', - 'mermaid', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'merboy', - 'mergirl', - 'merman', - 'merperson', - 'selkie', - 'undine', - 'atargatis', - 'siren' - ]), - Emoji( - name: 'merperson: light skin tone', - char: '\u{1F9DC}\u{1F3FB}', - shortName: 'merperson_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'light skin tone', - 'mermaid', - 'merman', - 'merwoman', - 'uc10', - 'diversity', - 'halloween', - 'mermaid', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'merboy', - 'mergirl', - 'merman', - 'merperson', - 'selkie', - 'undine', - 'atargatis', - 'siren' - ], - modifiable: true), - Emoji( - name: 'merperson: medium-light skin tone', - char: '\u{1F9DC}\u{1F3FC}', - shortName: 'merperson_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'medium-light skin tone', - 'mermaid', - 'merman', - 'merwoman', - 'uc10', - 'diversity', - 'halloween', - 'mermaid', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'merboy', - 'mergirl', - 'merman', - 'merperson', - 'selkie', - 'undine', - 'atargatis', - 'siren' - ], - modifiable: true), - Emoji( - name: 'merperson: medium skin tone', - char: '\u{1F9DC}\u{1F3FD}', - shortName: 'merperson_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'medium skin tone', - 'mermaid', - 'merman', - 'merwoman', - 'uc10', - 'diversity', - 'halloween', - 'mermaid', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'merboy', - 'mergirl', - 'merman', - 'merperson', - 'selkie', - 'undine', - 'atargatis', - 'siren' - ], - modifiable: true), - Emoji( - name: 'merperson: medium-dark skin tone', - char: '\u{1F9DC}\u{1F3FE}', - shortName: 'merperson_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'medium-dark skin tone', - 'mermaid', - 'merman', - 'merwoman', - 'uc10', - 'diversity', - 'halloween', - 'mermaid', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'merboy', - 'mergirl', - 'merman', - 'merperson', - 'selkie', - 'undine', - 'atargatis', - 'siren' - ], - modifiable: true), - Emoji( - name: 'merperson: dark skin tone', - char: '\u{1F9DC}\u{1F3FF}', - shortName: 'merperson_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'dark skin tone', - 'mermaid', - 'merman', - 'merwoman', - 'uc10', - 'diversity', - 'halloween', - 'mermaid', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'merboy', - 'mergirl', - 'merman', - 'merperson', - 'selkie', - 'undine', - 'atargatis', - 'siren' - ], - modifiable: true), - Emoji( - name: 'mermaid', - char: '\u{1F9DC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'mermaid', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'merwoman', - 'uc10', - 'diversity', - 'halloween', - 'disney', - 'mermaid', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'cartoon', - 'merboy', - 'mergirl', - 'merman', - 'merperson', - 'selkie', - 'undine', - 'atargatis', - 'siren' - ]), - Emoji( - name: 'mermaid: light skin tone', - char: '\u{1F9DC}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'mermaid_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'light skin tone', - 'merwoman', - 'uc10', - 'diversity', - 'halloween', - 'disney', - 'mermaid', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'cartoon', - 'merboy', - 'mergirl', - 'merman', - 'merperson', - 'selkie', - 'undine', - 'atargatis', - 'siren' - ], - modifiable: true), - Emoji( - name: 'mermaid: medium-light skin tone', - char: '\u{1F9DC}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'mermaid_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'medium-light skin tone', - 'merwoman', - 'uc10', - 'diversity', - 'halloween', - 'disney', - 'mermaid', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'cartoon', - 'merboy', - 'mergirl', - 'merman', - 'merperson', - 'selkie', - 'undine', - 'atargatis', - 'siren' - ], - modifiable: true), - Emoji( - name: 'mermaid: medium skin tone', - char: '\u{1F9DC}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'mermaid_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'medium skin tone', - 'merwoman', - 'uc10', - 'diversity', - 'halloween', - 'disney', - 'mermaid', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'cartoon', - 'merboy', - 'mergirl', - 'merman', - 'merperson', - 'selkie', - 'undine', - 'atargatis', - 'siren' - ], - modifiable: true), - Emoji( - name: 'mermaid: medium-dark skin tone', - char: '\u{1F9DC}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'mermaid_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'medium-dark skin tone', - 'merwoman', - 'uc10', - 'diversity', - 'halloween', - 'disney', - 'mermaid', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'cartoon', - 'merboy', - 'mergirl', - 'merman', - 'merperson', - 'selkie', - 'undine', - 'atargatis', - 'siren' - ], - modifiable: true), - Emoji( - name: 'mermaid: dark skin tone', - char: '\u{1F9DC}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'mermaid_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'dark skin tone', - 'merwoman', - 'uc10', - 'diversity', - 'halloween', - 'disney', - 'mermaid', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'cartoon', - 'merboy', - 'mergirl', - 'merman', - 'merperson', - 'selkie', - 'undine', - 'atargatis', - 'siren' - ], - modifiable: true), - Emoji( - name: 'merman', - char: '\u{1F9DC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'merman', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Triton', - 'uc10', - 'diversity', - 'halloween', - 'mermaid', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'merboy', - 'mergirl', - 'merman', - 'merperson', - 'selkie', - 'undine', - 'atargatis', - 'siren' - ]), - Emoji( - name: 'merman: light skin tone', - char: '\u{1F9DC}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'merman_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Triton', - 'light skin tone', - 'uc10', - 'diversity', - 'halloween', - 'mermaid', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'merboy', - 'mergirl', - 'merman', - 'merperson', - 'selkie', - 'undine', - 'atargatis', - 'siren' - ], - modifiable: true), - Emoji( - name: 'merman: medium-light skin tone', - char: '\u{1F9DC}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'merman_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Triton', - 'medium-light skin tone', - 'uc10', - 'diversity', - 'halloween', - 'mermaid', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'merboy', - 'mergirl', - 'merman', - 'merperson', - 'selkie', - 'undine', - 'atargatis', - 'siren' - ], - modifiable: true), - Emoji( - name: 'merman: medium skin tone', - char: '\u{1F9DC}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'merman_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Triton', - 'medium skin tone', - 'uc10', - 'diversity', - 'halloween', - 'mermaid', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'merboy', - 'mergirl', - 'merman', - 'merperson', - 'selkie', - 'undine', - 'atargatis', - 'siren' - ], - modifiable: true), - Emoji( - name: 'merman: medium-dark skin tone', - char: '\u{1F9DC}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'merman_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Triton', - 'medium-dark skin tone', - 'uc10', - 'diversity', - 'halloween', - 'mermaid', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'merboy', - 'mergirl', - 'merman', - 'merperson', - 'selkie', - 'undine', - 'atargatis', - 'siren' - ], - modifiable: true), - Emoji( - name: 'merman: dark skin tone', - char: '\u{1F9DC}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'merman_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Triton', - 'dark skin tone', - 'uc10', - 'diversity', - 'halloween', - 'mermaid', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'merboy', - 'mergirl', - 'merman', - 'merperson', - 'selkie', - 'undine', - 'atargatis', - 'siren' - ], - modifiable: true), - Emoji( - name: 'fairy', - char: '\u{1F9DA}', - shortName: 'fairy', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Oberon', - 'Puck', - 'Titania', - 'uc10', - 'diversity', - 'halloween', - 'magic', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'spell', - 'genie', - 'magical' - ]), - Emoji( - name: 'fairy: light skin tone', - char: '\u{1F9DA}\u{1F3FB}', - shortName: 'fairy_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Oberon', - 'Puck', - 'Titania', - 'light skin tone', - 'uc10', - 'diversity', - 'halloween', - 'magic', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'spell', - 'genie', - 'magical' - ], - modifiable: true), - Emoji( - name: 'fairy: medium-light skin tone', - char: '\u{1F9DA}\u{1F3FC}', - shortName: 'fairy_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Oberon', - 'Puck', - 'Titania', - 'medium-light skin tone', - 'uc10', - 'diversity', - 'halloween', - 'magic', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'spell', - 'genie', - 'magical' - ], - modifiable: true), - Emoji( - name: 'fairy: medium skin tone', - char: '\u{1F9DA}\u{1F3FD}', - shortName: 'fairy_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Oberon', - 'Puck', - 'Titania', - 'medium skin tone', - 'uc10', - 'diversity', - 'halloween', - 'magic', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'spell', - 'genie', - 'magical' - ], - modifiable: true), - Emoji( - name: 'fairy: medium-dark skin tone', - char: '\u{1F9DA}\u{1F3FE}', - shortName: 'fairy_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Oberon', - 'Puck', - 'Titania', - 'medium-dark skin tone', - 'uc10', - 'diversity', - 'halloween', - 'magic', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'spell', - 'genie', - 'magical' - ], - modifiable: true), - Emoji( - name: 'fairy: dark skin tone', - char: '\u{1F9DA}\u{1F3FF}', - shortName: 'fairy_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Oberon', - 'Puck', - 'Titania', - 'dark skin tone', - 'uc10', - 'diversity', - 'halloween', - 'magic', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'spell', - 'genie', - 'magical' - ], - modifiable: true), - Emoji( - name: 'woman fairy', - char: '\u{1F9DA}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_fairy', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Titania', - 'uc10', - 'diversity', - 'halloween', - 'beautiful', - 'magic', - 'disney', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'spell', - 'genie', - 'magical', - 'cartoon' - ]), - Emoji( - name: 'woman fairy: light skin tone', - char: '\u{1F9DA}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_fairy_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Titania', - 'light skin tone', - 'uc10', - 'diversity', - 'halloween', - 'beautiful', - 'magic', - 'disney', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'spell', - 'genie', - 'magical', - 'cartoon' - ], - modifiable: true), - Emoji( - name: 'woman fairy: medium-light skin tone', - char: '\u{1F9DA}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_fairy_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Titania', - 'medium-light skin tone', - 'uc10', - 'diversity', - 'halloween', - 'beautiful', - 'magic', - 'disney', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'spell', - 'genie', - 'magical', - 'cartoon' - ], - modifiable: true), - Emoji( - name: 'woman fairy: medium skin tone', - char: '\u{1F9DA}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_fairy_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Titania', - 'medium skin tone', - 'uc10', - 'diversity', - 'halloween', - 'beautiful', - 'magic', - 'disney', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'spell', - 'genie', - 'magical', - 'cartoon' - ], - modifiable: true), - Emoji( - name: 'woman fairy: medium-dark skin tone', - char: '\u{1F9DA}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_fairy_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Titania', - 'medium-dark skin tone', - 'uc10', - 'diversity', - 'halloween', - 'beautiful', - 'magic', - 'disney', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'spell', - 'genie', - 'magical', - 'cartoon' - ], - modifiable: true), - Emoji( - name: 'woman fairy: dark skin tone', - char: '\u{1F9DA}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_fairy_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Titania', - 'dark skin tone', - 'uc10', - 'diversity', - 'halloween', - 'beautiful', - 'magic', - 'disney', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'spell', - 'genie', - 'magical', - 'cartoon' - ], - modifiable: true), - Emoji( - name: 'man fairy', - char: '\u{1F9DA}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_fairy', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Oberon', - 'Puck', - 'uc10', - 'diversity', - 'halloween', - 'beautiful', - 'magic', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'spell', - 'genie', - 'magical' - ]), - Emoji( - name: 'man fairy: light skin tone', - char: '\u{1F9DA}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_fairy_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Oberon', - 'Puck', - 'light skin tone', - 'uc10', - 'diversity', - 'halloween', - 'beautiful', - 'magic', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'spell', - 'genie', - 'magical' - ], - modifiable: true), - Emoji( - name: 'man fairy: medium-light skin tone', - char: '\u{1F9DA}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_fairy_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Oberon', - 'Puck', - 'medium-light skin tone', - 'uc10', - 'diversity', - 'halloween', - 'beautiful', - 'magic', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'spell', - 'genie', - 'magical' - ], - modifiable: true), - Emoji( - name: 'man fairy: medium skin tone', - char: '\u{1F9DA}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_fairy_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Oberon', - 'Puck', - 'medium skin tone', - 'uc10', - 'diversity', - 'halloween', - 'beautiful', - 'magic', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'spell', - 'genie', - 'magical' - ], - modifiable: true), - Emoji( - name: 'man fairy: medium-dark skin tone', - char: '\u{1F9DA}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_fairy_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Oberon', - 'Puck', - 'medium-dark skin tone', - 'uc10', - 'diversity', - 'halloween', - 'beautiful', - 'magic', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'spell', - 'genie', - 'magical' - ], - modifiable: true), - Emoji( - name: 'man fairy: dark skin tone', - char: '\u{1F9DA}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_fairy_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'Oberon', - 'Puck', - 'dark skin tone', - 'uc10', - 'diversity', - 'halloween', - 'beautiful', - 'magic', - 'fantasy', - 'disguise', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'spell', - 'genie', - 'magical' - ], - modifiable: true), - Emoji( - name: 'baby angel', - char: '\u{1F47C}', - shortName: 'angel', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'angel', - 'baby', - 'face', - 'fairy tale', - 'fantasy', - 'uc6', - 'diversity', - 'halloween', - 'baby', - 'christmas', - 'pray', - 'omg', - 'jesus', - 'fantasy', - 'child', - 'soul', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'omfg', - 'oh my god', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ]), - Emoji( - name: 'baby angel: light skin tone', - char: '\u{1F47C}\u{1F3FB}', - shortName: 'angel_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'angel', - 'baby', - 'face', - 'fairy tale', - 'fantasy', - 'light skin tone', - 'uc8', - 'diversity', - 'halloween', - 'baby', - 'christmas', - 'pray', - 'omg', - 'jesus', - 'fantasy', - 'child', - 'soul', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'omfg', - 'oh my god', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'baby angel: medium-light skin tone', - char: '\u{1F47C}\u{1F3FC}', - shortName: 'angel_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'angel', - 'baby', - 'face', - 'fairy tale', - 'fantasy', - 'medium-light skin tone', - 'uc8', - 'diversity', - 'halloween', - 'baby', - 'christmas', - 'pray', - 'omg', - 'jesus', - 'fantasy', - 'child', - 'soul', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'omfg', - 'oh my god', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'baby angel: medium skin tone', - char: '\u{1F47C}\u{1F3FD}', - shortName: 'angel_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'angel', - 'baby', - 'face', - 'fairy tale', - 'fantasy', - 'medium skin tone', - 'uc8', - 'diversity', - 'halloween', - 'baby', - 'christmas', - 'pray', - 'omg', - 'jesus', - 'fantasy', - 'child', - 'soul', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'omfg', - 'oh my god', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'baby angel: medium-dark skin tone', - char: '\u{1F47C}\u{1F3FE}', - shortName: 'angel_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'angel', - 'baby', - 'face', - 'fairy tale', - 'fantasy', - 'medium-dark skin tone', - 'uc8', - 'diversity', - 'halloween', - 'baby', - 'christmas', - 'pray', - 'omg', - 'jesus', - 'fantasy', - 'child', - 'soul', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'omfg', - 'oh my god', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'baby angel: dark skin tone', - char: '\u{1F47C}\u{1F3FF}', - shortName: 'angel_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personFantasy, - keywords: [ - 'angel', - 'baby', - 'dark skin tone', - 'face', - 'fairy tale', - 'fantasy', - 'uc8', - 'diversity', - 'halloween', - 'baby', - 'christmas', - 'pray', - 'omg', - 'jesus', - 'fantasy', - 'child', - 'soul', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'samhain', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'omfg', - 'oh my god', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'pregnant woman', - char: '\u{1F930}', - shortName: 'pregnant_woman', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'pregnant', - 'woman', - 'uc9', - 'diversity', - 'women', - 'baby', - 'parent', - 'wife', - 'child', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'pregnant woman: light skin tone', - char: '\u{1F930}\u{1F3FB}', - shortName: 'pregnant_woman_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'light skin tone', - 'pregnant', - 'woman', - 'uc9', - 'diversity', - 'women', - 'baby', - 'parent', - 'wife', - 'child', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'pregnant woman: medium-light skin tone', - char: '\u{1F930}\u{1F3FC}', - shortName: 'pregnant_woman_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'medium-light skin tone', - 'pregnant', - 'woman', - 'uc9', - 'diversity', - 'women', - 'baby', - 'parent', - 'wife', - 'child', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'pregnant woman: medium skin tone', - char: '\u{1F930}\u{1F3FD}', - shortName: 'pregnant_woman_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'medium skin tone', - 'pregnant', - 'woman', - 'uc9', - 'diversity', - 'women', - 'baby', - 'parent', - 'wife', - 'child', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'pregnant woman: medium-dark skin tone', - char: '\u{1F930}\u{1F3FE}', - shortName: 'pregnant_woman_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'medium-dark skin tone', - 'pregnant', - 'woman', - 'uc9', - 'diversity', - 'women', - 'baby', - 'parent', - 'wife', - 'child', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'pregnant woman: dark skin tone', - char: '\u{1F930}\u{1F3FF}', - shortName: 'pregnant_woman_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'dark skin tone', - 'pregnant', - 'woman', - 'uc9', - 'diversity', - 'women', - 'baby', - 'parent', - 'wife', - 'child', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'breast-feeding', - char: '\u{1F931}', - shortName: 'breast_feeding', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'baby', - 'breast', - 'nursing', - 'uc10', - 'food', - 'diversity', - 'boobs', - 'women', - 'baby', - 'parent', - 'wife', - 'child', - 'mom', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'boob', - 'tits', - 'tit', - 'breast', - 'woman', - 'female', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'breast-feeding: light skin tone', - char: '\u{1F931}\u{1F3FB}', - shortName: 'breast_feeding_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'baby', - 'breast', - 'light skin tone', - 'nursing', - 'uc10', - 'food', - 'diversity', - 'boobs', - 'women', - 'baby', - 'parent', - 'wife', - 'child', - 'mom', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'boob', - 'tits', - 'tit', - 'breast', - 'woman', - 'female', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'breast-feeding: medium-light skin tone', - char: '\u{1F931}\u{1F3FC}', - shortName: 'breast_feeding_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'baby', - 'breast', - 'medium-light skin tone', - 'nursing', - 'uc10', - 'food', - 'diversity', - 'boobs', - 'women', - 'baby', - 'parent', - 'wife', - 'child', - 'mom', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'boob', - 'tits', - 'tit', - 'breast', - 'woman', - 'female', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'breast-feeding: medium skin tone', - char: '\u{1F931}\u{1F3FD}', - shortName: 'breast_feeding_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'baby', - 'breast', - 'medium skin tone', - 'nursing', - 'uc10', - 'food', - 'diversity', - 'boobs', - 'women', - 'baby', - 'parent', - 'wife', - 'child', - 'mom', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'boob', - 'tits', - 'tit', - 'breast', - 'woman', - 'female', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'breast-feeding: medium-dark skin tone', - char: '\u{1F931}\u{1F3FE}', - shortName: 'breast_feeding_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'baby', - 'breast', - 'medium-dark skin tone', - 'nursing', - 'uc10', - 'food', - 'diversity', - 'boobs', - 'women', - 'baby', - 'parent', - 'wife', - 'child', - 'mom', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'boob', - 'tits', - 'tit', - 'breast', - 'woman', - 'female', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'breast-feeding: dark skin tone', - char: '\u{1F931}\u{1F3FF}', - shortName: 'breast_feeding_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'baby', - 'breast', - 'dark skin tone', - 'nursing', - 'uc10', - 'food', - 'diversity', - 'boobs', - 'women', - 'baby', - 'parent', - 'wife', - 'child', - 'mom', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'boob', - 'tits', - 'tit', - 'breast', - 'woman', - 'female', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person feeding baby', - char: '\u{1F9D1}\u{200D}\u{1F37C}', - shortName: 'person_feeding_baby', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'food', - 'baby', - 'daddy', - 'parent', - 'wife', - 'child', - 'formula', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ]), - Emoji( - name: 'person feeding baby: light skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F37C}', - shortName: 'person_feeding_baby_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'food', - 'baby', - 'daddy', - 'parent', - 'wife', - 'child', - 'formula', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'person feeding baby: medium-light skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F37C}', - shortName: 'person_feeding_baby_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'food', - 'baby', - 'daddy', - 'parent', - 'wife', - 'child', - 'formula', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'person feeding baby: medium skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F37C}', - shortName: 'person_feeding_baby_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'food', - 'baby', - 'daddy', - 'parent', - 'wife', - 'child', - 'formula', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'person feeding baby: medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F37C}', - shortName: 'person_feeding_baby_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'food', - 'baby', - 'daddy', - 'parent', - 'wife', - 'child', - 'formula', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'person feeding baby: dark skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F37C}', - shortName: 'person_feeding_baby_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'food', - 'baby', - 'daddy', - 'parent', - 'wife', - 'child', - 'formula', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'woman feeding baby', - char: '\u{1F469}\u{200D}\u{1F37C}', - shortName: 'woman_feeding_baby', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'food', - 'baby', - 'parent', - 'wife', - 'child', - 'mom', - 'formula', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'woman feeding baby: light skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F37C}', - shortName: 'woman_feeding_baby_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'food', - 'baby', - 'parent', - 'wife', - 'child', - 'mom', - 'formula', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman feeding baby: medium-light skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F37C}', - shortName: 'woman_feeding_baby_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'food', - 'baby', - 'parent', - 'wife', - 'child', - 'mom', - 'formula', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman feeding baby: medium skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F37C}', - shortName: 'woman_feeding_baby_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'food', - 'baby', - 'parent', - 'wife', - 'child', - 'mom', - 'formula', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman feeding baby: medium-dark skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F37C}', - shortName: 'woman_feeding_baby_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'food', - 'baby', - 'parent', - 'wife', - 'child', - 'mom', - 'formula', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman feeding baby: dark skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F37C}', - shortName: 'woman_feeding_baby_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'food', - 'baby', - 'parent', - 'wife', - 'child', - 'mom', - 'formula', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'man feeding baby', - char: '\u{1F468}\u{200D}\u{1F37C}', - shortName: 'man_feeding_baby', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'food', - 'baby', - 'daddy', - 'parent', - 'child', - 'formula', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ]), - Emoji( - name: 'man feeding baby: light skin tone', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{1F37C}', - shortName: 'man_feeding_baby_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'food', - 'baby', - 'daddy', - 'parent', - 'child', - 'formula', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'man feeding baby: medium-light skin tone', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{1F37C}', - shortName: 'man_feeding_baby_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'food', - 'baby', - 'daddy', - 'parent', - 'child', - 'formula', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'man feeding baby: medium skin tone', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{1F37C}', - shortName: 'man_feeding_baby_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'food', - 'baby', - 'daddy', - 'parent', - 'child', - 'formula', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'man feeding baby: medium-dark skin tone', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{1F37C}', - shortName: 'man_feeding_baby_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'food', - 'baby', - 'daddy', - 'parent', - 'child', - 'formula', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'man feeding baby: dark skin tone', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{1F37C}', - shortName: 'man_feeding_baby_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personRole, - keywords: [ - 'uc13', - 'food', - 'baby', - 'daddy', - 'parent', - 'child', - 'formula', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ], - modifiable: true), - Emoji( - name: 'person bowing', - char: '\u{1F647}', - shortName: 'person_bowing', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'apology', - 'bow', - 'gesture', - 'sorry', - 'uc6', - 'diversity', - 'thank you', - 'pray', - 'jesus', - 'yoga', - 'begging', - 'fame', - 'idea', - 'hope', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'famous', - 'celebrity', - 'swear', - 'promise' - ]), - Emoji( - name: 'person bowing: light skin tone', - char: '\u{1F647}\u{1F3FB}', - shortName: 'person_bowing_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'apology', - 'bow', - 'gesture', - 'light skin tone', - 'sorry', - 'uc8', - 'diversity', - 'thank you', - 'pray', - 'jesus', - 'yoga', - 'begging', - 'fame', - 'idea', - 'hope', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'famous', - 'celebrity', - 'swear', - 'promise' - ], - modifiable: true), - Emoji( - name: 'person bowing: medium-light skin tone', - char: '\u{1F647}\u{1F3FC}', - shortName: 'person_bowing_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'apology', - 'bow', - 'gesture', - 'medium-light skin tone', - 'sorry', - 'uc8', - 'diversity', - 'thank you', - 'pray', - 'jesus', - 'yoga', - 'begging', - 'fame', - 'idea', - 'hope', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'famous', - 'celebrity', - 'swear', - 'promise' - ], - modifiable: true), - Emoji( - name: 'person bowing: medium skin tone', - char: '\u{1F647}\u{1F3FD}', - shortName: 'person_bowing_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'apology', - 'bow', - 'gesture', - 'medium skin tone', - 'sorry', - 'uc8', - 'diversity', - 'thank you', - 'pray', - 'jesus', - 'yoga', - 'begging', - 'fame', - 'idea', - 'hope', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'famous', - 'celebrity', - 'swear', - 'promise' - ], - modifiable: true), - Emoji( - name: 'person bowing: medium-dark skin tone', - char: '\u{1F647}\u{1F3FE}', - shortName: 'person_bowing_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'apology', - 'bow', - 'gesture', - 'medium-dark skin tone', - 'sorry', - 'uc8', - 'diversity', - 'thank you', - 'pray', - 'jesus', - 'yoga', - 'begging', - 'fame', - 'idea', - 'hope', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'famous', - 'celebrity', - 'swear', - 'promise' - ], - modifiable: true), - Emoji( - name: 'person bowing: dark skin tone', - char: '\u{1F647}\u{1F3FF}', - shortName: 'person_bowing_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'apology', - 'bow', - 'dark skin tone', - 'gesture', - 'sorry', - 'uc8', - 'diversity', - 'thank you', - 'pray', - 'jesus', - 'yoga', - 'begging', - 'fame', - 'idea', - 'hope', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'famous', - 'celebrity', - 'swear', - 'promise' - ], - modifiable: true), - Emoji( - name: 'woman bowing', - char: '\u{1F647}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_bowing', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'apology', - 'bowing', - 'favor', - 'gesture', - 'sorry', - 'woman', - 'uc6', - 'diversity', - 'women', - 'thank you', - 'pray', - 'jesus', - 'yoga', - 'fame', - 'idea', - 'hope', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'famous', - 'celebrity', - 'swear', - 'promise' - ]), - Emoji( - name: 'woman bowing: light skin tone', - char: '\u{1F647}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_bowing_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'apology', - 'bowing', - 'favor', - 'gesture', - 'light skin tone', - 'sorry', - 'woman', - 'uc8', - 'diversity', - 'women', - 'thank you', - 'pray', - 'jesus', - 'yoga', - 'fame', - 'idea', - 'hope', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'famous', - 'celebrity', - 'swear', - 'promise' - ], - modifiable: true), - Emoji( - name: 'woman bowing: medium-light skin tone', - char: '\u{1F647}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_bowing_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'apology', - 'bowing', - 'favor', - 'gesture', - 'medium-light skin tone', - 'sorry', - 'woman', - 'uc8', - 'diversity', - 'women', - 'thank you', - 'pray', - 'jesus', - 'yoga', - 'fame', - 'idea', - 'hope', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'famous', - 'celebrity', - 'swear', - 'promise' - ], - modifiable: true), - Emoji( - name: 'woman bowing: medium skin tone', - char: '\u{1F647}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_bowing_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'apology', - 'bowing', - 'favor', - 'gesture', - 'medium skin tone', - 'sorry', - 'woman', - 'uc8', - 'diversity', - 'women', - 'thank you', - 'pray', - 'jesus', - 'yoga', - 'fame', - 'idea', - 'hope', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'famous', - 'celebrity', - 'swear', - 'promise' - ], - modifiable: true), - Emoji( - name: 'woman bowing: medium-dark skin tone', - char: '\u{1F647}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_bowing_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'apology', - 'bowing', - 'favor', - 'gesture', - 'medium-dark skin tone', - 'sorry', - 'woman', - 'uc8', - 'diversity', - 'women', - 'thank you', - 'pray', - 'jesus', - 'yoga', - 'fame', - 'idea', - 'hope', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'famous', - 'celebrity', - 'swear', - 'promise' - ], - modifiable: true), - Emoji( - name: 'woman bowing: dark skin tone', - char: '\u{1F647}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_bowing_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'apology', - 'bowing', - 'dark skin tone', - 'favor', - 'gesture', - 'sorry', - 'woman', - 'uc8', - 'diversity', - 'women', - 'thank you', - 'pray', - 'jesus', - 'yoga', - 'fame', - 'idea', - 'hope', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'famous', - 'celebrity', - 'swear', - 'promise' - ], - modifiable: true), - Emoji( - name: 'man bowing', - char: '\u{1F647}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_bowing', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'apology', - 'bowing', - 'favor', - 'gesture', - 'man', - 'sorry', - 'uc6', - 'diversity', - 'thank you', - 'pray', - 'jesus', - 'yoga', - 'fame', - 'idea', - 'hope', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'famous', - 'celebrity', - 'swear', - 'promise' - ]), - Emoji( - name: 'man bowing: light skin tone', - char: '\u{1F647}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_bowing_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'apology', - 'bowing', - 'favor', - 'gesture', - 'light skin tone', - 'man', - 'sorry', - 'uc8', - 'diversity', - 'thank you', - 'pray', - 'jesus', - 'yoga', - 'fame', - 'idea', - 'hope', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'famous', - 'celebrity', - 'swear', - 'promise' - ], - modifiable: true), - Emoji( - name: 'man bowing: medium-light skin tone', - char: '\u{1F647}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_bowing_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'apology', - 'bowing', - 'favor', - 'gesture', - 'man', - 'medium-light skin tone', - 'sorry', - 'uc8', - 'diversity', - 'thank you', - 'pray', - 'jesus', - 'yoga', - 'fame', - 'idea', - 'hope', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'famous', - 'celebrity', - 'swear', - 'promise' - ], - modifiable: true), - Emoji( - name: 'man bowing: medium skin tone', - char: '\u{1F647}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_bowing_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'apology', - 'bowing', - 'favor', - 'gesture', - 'man', - 'medium skin tone', - 'sorry', - 'uc8', - 'diversity', - 'thank you', - 'pray', - 'jesus', - 'yoga', - 'fame', - 'idea', - 'hope', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'famous', - 'celebrity', - 'swear', - 'promise' - ], - modifiable: true), - Emoji( - name: 'man bowing: medium-dark skin tone', - char: '\u{1F647}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_bowing_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'apology', - 'bowing', - 'favor', - 'gesture', - 'man', - 'medium-dark skin tone', - 'sorry', - 'uc8', - 'diversity', - 'thank you', - 'pray', - 'jesus', - 'yoga', - 'fame', - 'idea', - 'hope', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'famous', - 'celebrity', - 'swear', - 'promise' - ], - modifiable: true), - Emoji( - name: 'man bowing: dark skin tone', - char: '\u{1F647}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_bowing_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'apology', - 'bowing', - 'dark skin tone', - 'favor', - 'gesture', - 'man', - 'sorry', - 'uc8', - 'diversity', - 'thank you', - 'pray', - 'jesus', - 'yoga', - 'fame', - 'idea', - 'hope', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'famous', - 'celebrity', - 'swear', - 'promise' - ], - modifiable: true), - Emoji( - name: 'person tipping hand', - char: '\u{1F481}', - shortName: 'person_tipping_hand', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'hand', - 'help', - 'information', - 'sassy', - 'tipping', - 'uc6', - 'diversity', - 'men', - 'lipstick', - 'help', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'mouth', - 'mouths', - 'makeup', - 'lip', - 'lips', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'person tipping hand: light skin tone', - char: '\u{1F481}\u{1F3FB}', - shortName: 'person_tipping_hand_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'hand', - 'help', - 'information', - 'light skin tone', - 'sassy', - 'tipping', - 'uc8', - 'diversity', - 'men', - 'lipstick', - 'help', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'mouth', - 'mouths', - 'makeup', - 'lip', - 'lips', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person tipping hand: medium-light skin tone', - char: '\u{1F481}\u{1F3FC}', - shortName: 'person_tipping_hand_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'hand', - 'help', - 'information', - 'medium-light skin tone', - 'sassy', - 'tipping', - 'uc8', - 'diversity', - 'men', - 'lipstick', - 'help', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'mouth', - 'mouths', - 'makeup', - 'lip', - 'lips', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person tipping hand: medium skin tone', - char: '\u{1F481}\u{1F3FD}', - shortName: 'person_tipping_hand_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'hand', - 'help', - 'information', - 'medium skin tone', - 'sassy', - 'tipping', - 'uc8', - 'diversity', - 'men', - 'lipstick', - 'help', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'mouth', - 'mouths', - 'makeup', - 'lip', - 'lips', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person tipping hand: medium-dark skin tone', - char: '\u{1F481}\u{1F3FE}', - shortName: 'person_tipping_hand_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'hand', - 'help', - 'information', - 'medium-dark skin tone', - 'sassy', - 'tipping', - 'uc8', - 'diversity', - 'men', - 'lipstick', - 'help', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'mouth', - 'mouths', - 'makeup', - 'lip', - 'lips', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person tipping hand: dark skin tone', - char: '\u{1F481}\u{1F3FF}', - shortName: 'person_tipping_hand_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'dark skin tone', - 'hand', - 'help', - 'information', - 'sassy', - 'tipping', - 'uc8', - 'diversity', - 'men', - 'lipstick', - 'help', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'mouth', - 'mouths', - 'makeup', - 'lip', - 'lips', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman tipping hand', - char: '\u{1F481}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_tipping_hand', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'sassy', - 'tipping hand', - 'woman', - 'uc6', - 'diversity', - 'women', - 'help', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'woman tipping hand: light skin tone', - char: '\u{1F481}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_tipping_hand_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'light skin tone', - 'sassy', - 'tipping hand', - 'woman', - 'uc8', - 'diversity', - 'women', - 'help', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman tipping hand: medium-light skin tone', - char: '\u{1F481}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_tipping_hand_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'medium-light skin tone', - 'sassy', - 'tipping hand', - 'woman', - 'uc8', - 'diversity', - 'women', - 'help', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman tipping hand: medium skin tone', - char: '\u{1F481}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_tipping_hand_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'medium skin tone', - 'sassy', - 'tipping hand', - 'woman', - 'uc8', - 'diversity', - 'women', - 'help', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman tipping hand: medium-dark skin tone', - char: '\u{1F481}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_tipping_hand_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'medium-dark skin tone', - 'sassy', - 'tipping hand', - 'woman', - 'uc8', - 'diversity', - 'women', - 'help', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman tipping hand: dark skin tone', - char: '\u{1F481}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_tipping_hand_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'dark skin tone', - 'sassy', - 'tipping hand', - 'woman', - 'uc8', - 'diversity', - 'women', - 'help', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'man tipping hand', - char: '\u{1F481}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_tipping_hand', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'man', - 'sassy', - 'tipping hand', - 'uc6', - 'diversity', - 'men', - 'help', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male' - ]), - Emoji( - name: 'man tipping hand: light skin tone', - char: '\u{1F481}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_tipping_hand_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'light skin tone', - 'man', - 'sassy', - 'tipping hand', - 'uc8', - 'diversity', - 'men', - 'help', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male' - ], - modifiable: true), - Emoji( - name: 'man tipping hand: medium-light skin tone', - char: '\u{1F481}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_tipping_hand_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'man', - 'medium-light skin tone', - 'sassy', - 'tipping hand', - 'uc8', - 'diversity', - 'men', - 'help', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male' - ], - modifiable: true), - Emoji( - name: 'man tipping hand: medium skin tone', - char: '\u{1F481}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_tipping_hand_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'man', - 'medium skin tone', - 'sassy', - 'tipping hand', - 'uc8', - 'diversity', - 'men', - 'help', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male' - ], - modifiable: true), - Emoji( - name: 'man tipping hand: medium-dark skin tone', - char: '\u{1F481}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_tipping_hand_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'man', - 'medium-dark skin tone', - 'sassy', - 'tipping hand', - 'uc8', - 'diversity', - 'men', - 'help', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male' - ], - modifiable: true), - Emoji( - name: 'man tipping hand: dark skin tone', - char: '\u{1F481}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_tipping_hand_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'dark skin tone', - 'man', - 'sassy', - 'tipping hand', - 'uc8', - 'diversity', - 'men', - 'help', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male' - ], - modifiable: true), - Emoji( - name: 'person gesturing NO', - char: '\u{1F645}', - shortName: 'person_gesturing_no', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'forbidden', - 'gesture', - 'hand', - 'no', - 'not', - 'prohibited', - 'uc6', - 'diversity', - 'men', - 'angry', - 'girls night', - 'hate', - 'danger', - 'bitch', - 'daddy', - 'crazy', - 'private', - 'mom', - 'never', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'ladies night', - 'girls only', - 'girlfriend', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'puta', - 'pute', - 'dad', - 'papa', - 'pere', - 'father', - 'weird', - 'awkward', - 'insane', - 'wild', - 'прив', - 'privé', - 'privado', - 'reserved', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'person gesturing NO: light skin tone', - char: '\u{1F645}\u{1F3FB}', - shortName: 'person_gesturing_no_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'forbidden', - 'gesture', - 'hand', - 'light skin tone', - 'no', - 'not', - 'prohibited', - 'uc8', - 'diversity', - 'men', - 'angry', - 'girls night', - 'hate', - 'danger', - 'bitch', - 'daddy', - 'crazy', - 'private', - 'mom', - 'never', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'ladies night', - 'girls only', - 'girlfriend', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'puta', - 'pute', - 'dad', - 'papa', - 'pere', - 'father', - 'weird', - 'awkward', - 'insane', - 'wild', - 'прив', - 'privé', - 'privado', - 'reserved', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person gesturing NO: medium-light skin tone', - char: '\u{1F645}\u{1F3FC}', - shortName: 'person_gesturing_no_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'forbidden', - 'gesture', - 'hand', - 'medium-light skin tone', - 'no', - 'not', - 'prohibited', - 'uc8', - 'diversity', - 'men', - 'angry', - 'girls night', - 'hate', - 'danger', - 'bitch', - 'daddy', - 'crazy', - 'private', - 'mom', - 'never', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'ladies night', - 'girls only', - 'girlfriend', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'puta', - 'pute', - 'dad', - 'papa', - 'pere', - 'father', - 'weird', - 'awkward', - 'insane', - 'wild', - 'прив', - 'privé', - 'privado', - 'reserved', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person gesturing NO: medium skin tone', - char: '\u{1F645}\u{1F3FD}', - shortName: 'person_gesturing_no_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'forbidden', - 'gesture', - 'hand', - 'medium skin tone', - 'no', - 'not', - 'prohibited', - 'uc8', - 'diversity', - 'men', - 'angry', - 'girls night', - 'hate', - 'danger', - 'bitch', - 'daddy', - 'crazy', - 'private', - 'mom', - 'never', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'ladies night', - 'girls only', - 'girlfriend', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'puta', - 'pute', - 'dad', - 'papa', - 'pere', - 'father', - 'weird', - 'awkward', - 'insane', - 'wild', - 'прив', - 'privé', - 'privado', - 'reserved', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person gesturing NO: medium-dark skin tone', - char: '\u{1F645}\u{1F3FE}', - shortName: 'person_gesturing_no_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'forbidden', - 'gesture', - 'hand', - 'medium-dark skin tone', - 'no', - 'not', - 'prohibited', - 'uc8', - 'diversity', - 'men', - 'angry', - 'girls night', - 'hate', - 'danger', - 'bitch', - 'daddy', - 'crazy', - 'private', - 'mom', - 'never', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'ladies night', - 'girls only', - 'girlfriend', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'puta', - 'pute', - 'dad', - 'papa', - 'pere', - 'father', - 'weird', - 'awkward', - 'insane', - 'wild', - 'прив', - 'privé', - 'privado', - 'reserved', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person gesturing NO: dark skin tone', - char: '\u{1F645}\u{1F3FF}', - shortName: 'person_gesturing_no_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'dark skin tone', - 'forbidden', - 'gesture', - 'hand', - 'no', - 'not', - 'prohibited', - 'uc8', - 'diversity', - 'men', - 'angry', - 'girls night', - 'hate', - 'danger', - 'bitch', - 'daddy', - 'crazy', - 'private', - 'mom', - 'never', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'ladies night', - 'girls only', - 'girlfriend', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'puta', - 'pute', - 'dad', - 'papa', - 'pere', - 'father', - 'weird', - 'awkward', - 'insane', - 'wild', - 'прив', - 'privé', - 'privado', - 'reserved', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman gesturing NO', - char: '\u{1F645}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_gesturing_no', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'forbidden', - 'gesture', - 'hand', - 'no', - 'prohibited', - 'woman', - 'uc6', - 'diversity', - 'women', - 'girls night', - 'danger', - 'bitch', - 'crazy', - 'private', - 'mom', - 'never', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'ladies night', - 'girls only', - 'girlfriend', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'puta', - 'pute', - 'weird', - 'awkward', - 'insane', - 'wild', - 'прив', - 'privé', - 'privado', - 'reserved', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'woman gesturing NO: light skin tone', - char: '\u{1F645}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_gesturing_no_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'forbidden', - 'gesture', - 'hand', - 'light skin tone', - 'no', - 'prohibited', - 'woman', - 'uc8', - 'diversity', - 'women', - 'girls night', - 'danger', - 'bitch', - 'crazy', - 'private', - 'mom', - 'never', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'ladies night', - 'girls only', - 'girlfriend', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'puta', - 'pute', - 'weird', - 'awkward', - 'insane', - 'wild', - 'прив', - 'privé', - 'privado', - 'reserved', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman gesturing NO: medium-light skin tone', - char: '\u{1F645}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_gesturing_no_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'forbidden', - 'gesture', - 'hand', - 'medium-light skin tone', - 'no', - 'prohibited', - 'woman', - 'uc8', - 'diversity', - 'women', - 'girls night', - 'danger', - 'bitch', - 'crazy', - 'private', - 'mom', - 'never', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'ladies night', - 'girls only', - 'girlfriend', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'puta', - 'pute', - 'weird', - 'awkward', - 'insane', - 'wild', - 'прив', - 'privé', - 'privado', - 'reserved', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman gesturing NO: medium skin tone', - char: '\u{1F645}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_gesturing_no_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'forbidden', - 'gesture', - 'hand', - 'medium skin tone', - 'no', - 'prohibited', - 'woman', - 'uc8', - 'diversity', - 'women', - 'girls night', - 'danger', - 'bitch', - 'crazy', - 'private', - 'mom', - 'never', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'ladies night', - 'girls only', - 'girlfriend', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'puta', - 'pute', - 'weird', - 'awkward', - 'insane', - 'wild', - 'прив', - 'privé', - 'privado', - 'reserved', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman gesturing NO: medium-dark skin tone', - char: '\u{1F645}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_gesturing_no_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'forbidden', - 'gesture', - 'hand', - 'medium-dark skin tone', - 'no', - 'prohibited', - 'woman', - 'uc8', - 'diversity', - 'women', - 'girls night', - 'danger', - 'bitch', - 'crazy', - 'private', - 'mom', - 'never', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'ladies night', - 'girls only', - 'girlfriend', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'puta', - 'pute', - 'weird', - 'awkward', - 'insane', - 'wild', - 'прив', - 'privé', - 'privado', - 'reserved', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman gesturing NO: dark skin tone', - char: '\u{1F645}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_gesturing_no_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'dark skin tone', - 'forbidden', - 'gesture', - 'hand', - 'no', - 'prohibited', - 'woman', - 'uc8', - 'diversity', - 'women', - 'girls night', - 'danger', - 'bitch', - 'crazy', - 'private', - 'mom', - 'never', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'ladies night', - 'girls only', - 'girlfriend', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'puta', - 'pute', - 'weird', - 'awkward', - 'insane', - 'wild', - 'прив', - 'privé', - 'privado', - 'reserved', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'man gesturing NO', - char: '\u{1F645}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_gesturing_no', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'forbidden', - 'gesture', - 'hand', - 'man', - 'no', - 'prohibited', - 'uc6', - 'diversity', - 'men', - 'angry', - 'hate', - 'danger', - 'daddy', - 'crazy', - 'private', - 'never', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'dad', - 'papa', - 'pere', - 'father', - 'weird', - 'awkward', - 'insane', - 'wild', - 'прив', - 'privé', - 'privado', - 'reserved' - ]), - Emoji( - name: 'man gesturing NO: light skin tone', - char: '\u{1F645}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_gesturing_no_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'forbidden', - 'gesture', - 'hand', - 'light skin tone', - 'man', - 'no', - 'prohibited', - 'uc8', - 'diversity', - 'men', - 'angry', - 'hate', - 'danger', - 'daddy', - 'crazy', - 'private', - 'never', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'dad', - 'papa', - 'pere', - 'father', - 'weird', - 'awkward', - 'insane', - 'wild', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'man gesturing NO: medium-light skin tone', - char: '\u{1F645}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_gesturing_no_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'forbidden', - 'gesture', - 'hand', - 'man', - 'medium-light skin tone', - 'no', - 'prohibited', - 'uc8', - 'diversity', - 'men', - 'angry', - 'hate', - 'danger', - 'daddy', - 'crazy', - 'private', - 'never', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'dad', - 'papa', - 'pere', - 'father', - 'weird', - 'awkward', - 'insane', - 'wild', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'man gesturing NO: medium skin tone', - char: '\u{1F645}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_gesturing_no_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'forbidden', - 'gesture', - 'hand', - 'man', - 'medium skin tone', - 'no', - 'prohibited', - 'uc8', - 'diversity', - 'men', - 'angry', - 'hate', - 'danger', - 'daddy', - 'crazy', - 'private', - 'never', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'dad', - 'papa', - 'pere', - 'father', - 'weird', - 'awkward', - 'insane', - 'wild', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'man gesturing NO: medium-dark skin tone', - char: '\u{1F645}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_gesturing_no_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'forbidden', - 'gesture', - 'hand', - 'man', - 'medium-dark skin tone', - 'no', - 'prohibited', - 'uc8', - 'diversity', - 'men', - 'angry', - 'hate', - 'danger', - 'daddy', - 'crazy', - 'private', - 'never', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'dad', - 'papa', - 'pere', - 'father', - 'weird', - 'awkward', - 'insane', - 'wild', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'man gesturing NO: dark skin tone', - char: '\u{1F645}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_gesturing_no_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'dark skin tone', - 'forbidden', - 'gesture', - 'hand', - 'man', - 'no', - 'prohibited', - 'uc8', - 'diversity', - 'men', - 'angry', - 'hate', - 'danger', - 'daddy', - 'crazy', - 'private', - 'never', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'dad', - 'papa', - 'pere', - 'father', - 'weird', - 'awkward', - 'insane', - 'wild', - 'прив', - 'privé', - 'privado', - 'reserved' - ], - modifiable: true), - Emoji( - name: 'person gesturing OK', - char: '\u{1F646}', - shortName: 'person_gesturing_ok', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'OK', - 'gesture', - 'hand', - 'uc6', - 'diversity', - 'men', - 'thank you', - 'awesome', - 'yoga', - 'crazy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'weird', - 'awkward', - 'insane', - 'wild', - '*\\0/*', - '\\0/', - '*\\O/*', - '\\O/' - ]), - Emoji( - name: 'person gesturing OK: light skin tone', - char: '\u{1F646}\u{1F3FB}', - shortName: 'person_gesturing_ok_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'OK', - 'gesture', - 'hand', - 'light skin tone', - 'uc8', - 'diversity', - 'men', - 'thank you', - 'awesome', - 'yoga', - 'crazy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'person gesturing OK: medium-light skin tone', - char: '\u{1F646}\u{1F3FC}', - shortName: 'person_gesturing_ok_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'OK', - 'gesture', - 'hand', - 'medium-light skin tone', - 'uc8', - 'diversity', - 'men', - 'thank you', - 'awesome', - 'yoga', - 'crazy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'person gesturing OK: medium skin tone', - char: '\u{1F646}\u{1F3FD}', - shortName: 'person_gesturing_ok_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'OK', - 'gesture', - 'hand', - 'medium skin tone', - 'uc8', - 'diversity', - 'men', - 'thank you', - 'awesome', - 'yoga', - 'crazy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'person gesturing OK: medium-dark skin tone', - char: '\u{1F646}\u{1F3FE}', - shortName: 'person_gesturing_ok_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'OK', - 'gesture', - 'hand', - 'medium-dark skin tone', - 'uc8', - 'diversity', - 'men', - 'thank you', - 'awesome', - 'yoga', - 'crazy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'person gesturing OK: dark skin tone', - char: '\u{1F646}\u{1F3FF}', - shortName: 'person_gesturing_ok_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'OK', - 'dark skin tone', - 'gesture', - 'hand', - 'uc8', - 'diversity', - 'men', - 'thank you', - 'awesome', - 'yoga', - 'crazy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'woman gesturing OK', - char: '\u{1F646}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_gesturing_ok', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'OK', - 'gesture', - 'hand', - 'woman', - 'uc6', - 'diversity', - 'women', - 'thank you', - 'awesome', - 'yoga', - 'crazy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'weird', - 'awkward', - 'insane', - 'wild' - ]), - Emoji( - name: 'woman gesturing OK: light skin tone', - char: '\u{1F646}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_gesturing_ok_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'OK', - 'gesture', - 'hand', - 'light skin tone', - 'woman', - 'uc8', - 'diversity', - 'women', - 'thank you', - 'awesome', - 'yoga', - 'crazy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'woman gesturing OK: medium-light skin tone', - char: '\u{1F646}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_gesturing_ok_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'OK', - 'gesture', - 'hand', - 'medium-light skin tone', - 'woman', - 'uc8', - 'diversity', - 'women', - 'thank you', - 'awesome', - 'yoga', - 'crazy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'woman gesturing OK: medium skin tone', - char: '\u{1F646}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_gesturing_ok_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'OK', - 'gesture', - 'hand', - 'medium skin tone', - 'woman', - 'uc8', - 'diversity', - 'women', - 'thank you', - 'awesome', - 'yoga', - 'crazy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'woman gesturing OK: medium-dark skin tone', - char: '\u{1F646}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_gesturing_ok_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'OK', - 'gesture', - 'hand', - 'medium-dark skin tone', - 'woman', - 'uc8', - 'diversity', - 'women', - 'thank you', - 'awesome', - 'yoga', - 'crazy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'woman gesturing OK: dark skin tone', - char: '\u{1F646}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_gesturing_ok_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'OK', - 'dark skin tone', - 'gesture', - 'hand', - 'woman', - 'uc8', - 'diversity', - 'women', - 'thank you', - 'awesome', - 'yoga', - 'crazy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'man gesturing OK', - char: '\u{1F646}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_gesturing_ok', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'OK', - 'gesture', - 'hand', - 'man', - 'uc6', - 'diversity', - 'men', - 'thank you', - 'awesome', - 'yoga', - 'crazy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'weird', - 'awkward', - 'insane', - 'wild' - ]), - Emoji( - name: 'man gesturing OK: light skin tone', - char: '\u{1F646}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_gesturing_ok_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'OK', - 'gesture', - 'hand', - 'light skin tone', - 'man', - 'uc8', - 'diversity', - 'men', - 'thank you', - 'awesome', - 'yoga', - 'crazy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'man gesturing OK: medium-light skin tone', - char: '\u{1F646}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_gesturing_ok_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'OK', - 'gesture', - 'hand', - 'man', - 'medium-light skin tone', - 'uc8', - 'diversity', - 'men', - 'thank you', - 'awesome', - 'yoga', - 'crazy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'man gesturing OK: medium skin tone', - char: '\u{1F646}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_gesturing_ok_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'OK', - 'gesture', - 'hand', - 'man', - 'medium skin tone', - 'uc8', - 'diversity', - 'men', - 'thank you', - 'awesome', - 'yoga', - 'crazy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'man gesturing OK: medium-dark skin tone', - char: '\u{1F646}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_gesturing_ok_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'OK', - 'gesture', - 'hand', - 'man', - 'medium-dark skin tone', - 'uc8', - 'diversity', - 'men', - 'thank you', - 'awesome', - 'yoga', - 'crazy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'man gesturing OK: dark skin tone', - char: '\u{1F646}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_gesturing_ok_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'OK', - 'dark skin tone', - 'gesture', - 'hand', - 'man', - 'uc8', - 'diversity', - 'men', - 'thank you', - 'awesome', - 'yoga', - 'crazy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'person raising hand', - char: '\u{1F64B}', - shortName: 'person_raising_hand', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'hand', - 'happy', - 'raised', - 'uc6', - 'diversity', - 'men', - 'hi', - 'girls night', - 'boys night', - 'celebrate', - 'daddy', - 'help', - 'crazy', - 'mom', - 'proud', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'ladies night', - 'girls only', - 'girlfriend', - 'guys night', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'dad', - 'papa', - 'pere', - 'father', - 'weird', - 'awkward', - 'insane', - 'wild', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'person raising hand: light skin tone', - char: '\u{1F64B}\u{1F3FB}', - shortName: 'person_raising_hand_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'hand', - 'happy', - 'light skin tone', - 'raised', - 'uc8', - 'diversity', - 'men', - 'hi', - 'girls night', - 'boys night', - 'celebrate', - 'daddy', - 'help', - 'crazy', - 'mom', - 'proud', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'ladies night', - 'girls only', - 'girlfriend', - 'guys night', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'dad', - 'papa', - 'pere', - 'father', - 'weird', - 'awkward', - 'insane', - 'wild', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person raising hand: medium-light skin tone', - char: '\u{1F64B}\u{1F3FC}', - shortName: 'person_raising_hand_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'hand', - 'happy', - 'medium-light skin tone', - 'raised', - 'uc8', - 'diversity', - 'men', - 'hi', - 'girls night', - 'boys night', - 'celebrate', - 'daddy', - 'help', - 'crazy', - 'mom', - 'proud', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'ladies night', - 'girls only', - 'girlfriend', - 'guys night', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'dad', - 'papa', - 'pere', - 'father', - 'weird', - 'awkward', - 'insane', - 'wild', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person raising hand: medium skin tone', - char: '\u{1F64B}\u{1F3FD}', - shortName: 'person_raising_hand_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'hand', - 'happy', - 'medium skin tone', - 'raised', - 'uc8', - 'diversity', - 'men', - 'hi', - 'girls night', - 'boys night', - 'celebrate', - 'daddy', - 'help', - 'crazy', - 'mom', - 'proud', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'ladies night', - 'girls only', - 'girlfriend', - 'guys night', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'dad', - 'papa', - 'pere', - 'father', - 'weird', - 'awkward', - 'insane', - 'wild', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person raising hand: medium-dark skin tone', - char: '\u{1F64B}\u{1F3FE}', - shortName: 'person_raising_hand_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'hand', - 'happy', - 'medium-dark skin tone', - 'raised', - 'uc8', - 'diversity', - 'men', - 'hi', - 'girls night', - 'boys night', - 'celebrate', - 'daddy', - 'help', - 'crazy', - 'mom', - 'proud', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'ladies night', - 'girls only', - 'girlfriend', - 'guys night', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'dad', - 'papa', - 'pere', - 'father', - 'weird', - 'awkward', - 'insane', - 'wild', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person raising hand: dark skin tone', - char: '\u{1F64B}\u{1F3FF}', - shortName: 'person_raising_hand_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'dark skin tone', - 'gesture', - 'hand', - 'happy', - 'raised', - 'uc8', - 'diversity', - 'men', - 'hi', - 'girls night', - 'boys night', - 'celebrate', - 'daddy', - 'help', - 'crazy', - 'mom', - 'proud', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'ladies night', - 'girls only', - 'girlfriend', - 'guys night', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'dad', - 'papa', - 'pere', - 'father', - 'weird', - 'awkward', - 'insane', - 'wild', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman raising hand', - char: '\u{1F64B}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_raising_hand', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'raising hand', - 'woman', - 'uc6', - 'diversity', - 'women', - 'hi', - 'girls night', - 'celebrate', - 'help', - 'crazy', - 'mom', - 'proud', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'ladies night', - 'girls only', - 'girlfriend', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'weird', - 'awkward', - 'insane', - 'wild', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'woman raising hand: light skin tone', - char: '\u{1F64B}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_raising_hand_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'light skin tone', - 'raising hand', - 'woman', - 'uc8', - 'diversity', - 'women', - 'hi', - 'girls night', - 'celebrate', - 'help', - 'crazy', - 'mom', - 'proud', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'ladies night', - 'girls only', - 'girlfriend', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'weird', - 'awkward', - 'insane', - 'wild', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman raising hand: medium-light skin tone', - char: '\u{1F64B}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_raising_hand_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'medium-light skin tone', - 'raising hand', - 'woman', - 'uc8', - 'diversity', - 'women', - 'hi', - 'girls night', - 'celebrate', - 'help', - 'crazy', - 'mom', - 'proud', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'ladies night', - 'girls only', - 'girlfriend', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'weird', - 'awkward', - 'insane', - 'wild', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman raising hand: medium skin tone', - char: '\u{1F64B}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_raising_hand_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'medium skin tone', - 'raising hand', - 'woman', - 'uc8', - 'diversity', - 'women', - 'hi', - 'girls night', - 'celebrate', - 'help', - 'crazy', - 'mom', - 'proud', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'ladies night', - 'girls only', - 'girlfriend', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'weird', - 'awkward', - 'insane', - 'wild', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman raising hand: medium-dark skin tone', - char: '\u{1F64B}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_raising_hand_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'medium-dark skin tone', - 'raising hand', - 'woman', - 'uc8', - 'diversity', - 'women', - 'hi', - 'girls night', - 'celebrate', - 'help', - 'crazy', - 'mom', - 'proud', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'ladies night', - 'girls only', - 'girlfriend', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'weird', - 'awkward', - 'insane', - 'wild', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman raising hand: dark skin tone', - char: '\u{1F64B}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_raising_hand_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'dark skin tone', - 'gesture', - 'raising hand', - 'woman', - 'uc8', - 'diversity', - 'women', - 'hi', - 'girls night', - 'celebrate', - 'help', - 'crazy', - 'mom', - 'proud', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'ladies night', - 'girls only', - 'girlfriend', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'weird', - 'awkward', - 'insane', - 'wild', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'man raising hand', - char: '\u{1F64B}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_raising_hand', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'man', - 'raising hand', - 'uc6', - 'diversity', - 'men', - 'hi', - 'boys night', - 'celebrate', - 'daddy', - 'help', - 'crazy', - 'proud', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'guys night', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'dad', - 'papa', - 'pere', - 'father', - 'weird', - 'awkward', - 'insane', - 'wild' - ]), - Emoji( - name: 'man raising hand: light skin tone', - char: '\u{1F64B}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_raising_hand_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'light skin tone', - 'man', - 'raising hand', - 'uc8', - 'diversity', - 'men', - 'hi', - 'boys night', - 'celebrate', - 'daddy', - 'help', - 'crazy', - 'proud', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'guys night', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'dad', - 'papa', - 'pere', - 'father', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'man raising hand: medium-light skin tone', - char: '\u{1F64B}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_raising_hand_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'man', - 'medium-light skin tone', - 'raising hand', - 'uc8', - 'diversity', - 'men', - 'hi', - 'boys night', - 'celebrate', - 'daddy', - 'help', - 'crazy', - 'proud', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'guys night', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'dad', - 'papa', - 'pere', - 'father', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'man raising hand: medium skin tone', - char: '\u{1F64B}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_raising_hand_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'man', - 'medium skin tone', - 'raising hand', - 'uc8', - 'diversity', - 'men', - 'hi', - 'boys night', - 'celebrate', - 'daddy', - 'help', - 'crazy', - 'proud', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'guys night', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'dad', - 'papa', - 'pere', - 'father', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'man raising hand: medium-dark skin tone', - char: '\u{1F64B}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_raising_hand_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'man', - 'medium-dark skin tone', - 'raising hand', - 'uc8', - 'diversity', - 'men', - 'hi', - 'boys night', - 'celebrate', - 'daddy', - 'help', - 'crazy', - 'proud', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'guys night', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'dad', - 'papa', - 'pere', - 'father', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'man raising hand: dark skin tone', - char: '\u{1F64B}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_raising_hand_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'dark skin tone', - 'gesture', - 'man', - 'raising hand', - 'uc8', - 'diversity', - 'men', - 'hi', - 'boys night', - 'celebrate', - 'daddy', - 'help', - 'crazy', - 'proud', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'hello', - 'greeting', - 'bonjour', - 'bye', - 'ciao', - 'adios', - 'goodbye', - 'hey', - 'holla', - 'my name is', - 'salut', - 'welcome', - 'ПРИВЕТ', - 'tu tapelle', - 'guys night', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'dad', - 'papa', - 'pere', - 'father', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'deaf person', - char: '\u{1F9CF}', - shortName: 'deaf_person', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'quiet', - 'sound', - 'deaf', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear', - 'hard of hearing' - ]), - Emoji( - name: 'deaf person: light skin tone', - char: '\u{1F9CF}\u{1F3FB}', - shortName: 'deaf_person_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'quiet', - 'sound', - 'deaf', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear', - 'hard of hearing' - ], - modifiable: true), - Emoji( - name: 'deaf person: medium-light skin tone', - char: '\u{1F9CF}\u{1F3FC}', - shortName: 'deaf_person_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'quiet', - 'sound', - 'deaf', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear', - 'hard of hearing' - ], - modifiable: true), - Emoji( - name: 'deaf person: medium skin tone', - char: '\u{1F9CF}\u{1F3FD}', - shortName: 'deaf_person_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'quiet', - 'sound', - 'deaf', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear', - 'hard of hearing' - ], - modifiable: true), - Emoji( - name: 'deaf person: medium-dark skin tone', - char: '\u{1F9CF}\u{1F3FE}', - shortName: 'deaf_person_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'quiet', - 'sound', - 'deaf', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear', - 'hard of hearing' - ], - modifiable: true), - Emoji( - name: 'deaf person: dark skin tone', - char: '\u{1F9CF}\u{1F3FF}', - shortName: 'deaf_person_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'quiet', - 'sound', - 'deaf', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear', - 'hard of hearing' - ], - modifiable: true), - Emoji( - name: 'deaf woman', - char: '\u{1F9CF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'deaf_woman', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'women', - 'handicap', - 'quiet', - 'sound', - 'deaf', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'disabled', - 'disability', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear', - 'hard of hearing' - ]), - Emoji( - name: 'deaf woman: light skin tone', - char: '\u{1F9CF}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'deaf_woman_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'women', - 'handicap', - 'quiet', - 'sound', - 'deaf', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'disabled', - 'disability', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear', - 'hard of hearing' - ], - modifiable: true), - Emoji( - name: 'deaf woman: medium-light skin tone', - char: '\u{1F9CF}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'deaf_woman_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'women', - 'handicap', - 'quiet', - 'sound', - 'deaf', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'disabled', - 'disability', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear', - 'hard of hearing' - ], - modifiable: true), - Emoji( - name: 'deaf woman: medium skin tone', - char: '\u{1F9CF}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'deaf_woman_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'women', - 'handicap', - 'quiet', - 'sound', - 'deaf', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'disabled', - 'disability', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear', - 'hard of hearing' - ], - modifiable: true), - Emoji( - name: 'deaf woman: medium-dark skin tone', - char: '\u{1F9CF}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'deaf_woman_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'women', - 'handicap', - 'quiet', - 'sound', - 'deaf', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'disabled', - 'disability', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear', - 'hard of hearing' - ], - modifiable: true), - Emoji( - name: 'deaf woman: dark skin tone', - char: '\u{1F9CF}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'deaf_woman_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'women', - 'handicap', - 'quiet', - 'sound', - 'deaf', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'disabled', - 'disability', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear', - 'hard of hearing' - ], - modifiable: true), - Emoji( - name: 'deaf man', - char: '\u{1F9CF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'deaf_man', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'quiet', - 'sound', - 'deaf', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear', - 'hard of hearing' - ]), - Emoji( - name: 'deaf man: light skin tone', - char: '\u{1F9CF}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'deaf_man_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'quiet', - 'sound', - 'deaf', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear', - 'hard of hearing' - ], - modifiable: true), - Emoji( - name: 'deaf man: medium-light skin tone', - char: '\u{1F9CF}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'deaf_man_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'quiet', - 'sound', - 'deaf', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear', - 'hard of hearing' - ], - modifiable: true), - Emoji( - name: 'deaf man: medium skin tone', - char: '\u{1F9CF}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'deaf_man_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'quiet', - 'sound', - 'deaf', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear', - 'hard of hearing' - ], - modifiable: true), - Emoji( - name: 'deaf man: medium-dark skin tone', - char: '\u{1F9CF}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'deaf_man_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'quiet', - 'sound', - 'deaf', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear', - 'hard of hearing' - ], - modifiable: true), - Emoji( - name: 'deaf man: dark skin tone', - char: '\u{1F9CF}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'deaf_man_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'quiet', - 'sound', - 'deaf', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear', - 'hard of hearing' - ], - modifiable: true), - Emoji( - name: 'person facepalming', - char: '\u{1F926}', - shortName: 'person_facepalming', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'disbelief', - 'exasperation', - 'face', - 'palm', - 'uc9', - 'diversity', - 'men', - 'stressed', - 'boys night', - 'facepalm', - 'dumb', - 'las vegas', - 'crazy', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'whoops', - 'oops', - 'mistake', - 'idiot', - 'ignorant', - 'stupid', - 'vegas', - 'weird', - 'awkward', - 'insane', - 'wild' - ]), - Emoji( - name: 'person facepalming: light skin tone', - char: '\u{1F926}\u{1F3FB}', - shortName: 'person_facepalming_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'disbelief', - 'exasperation', - 'face', - 'light skin tone', - 'palm', - 'uc9', - 'diversity', - 'men', - 'stressed', - 'boys night', - 'facepalm', - 'dumb', - 'las vegas', - 'crazy', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'whoops', - 'oops', - 'mistake', - 'idiot', - 'ignorant', - 'stupid', - 'vegas', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'person facepalming: medium-light skin tone', - char: '\u{1F926}\u{1F3FC}', - shortName: 'person_facepalming_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'disbelief', - 'exasperation', - 'face', - 'medium-light skin tone', - 'palm', - 'uc9', - 'diversity', - 'men', - 'stressed', - 'boys night', - 'facepalm', - 'dumb', - 'las vegas', - 'crazy', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'whoops', - 'oops', - 'mistake', - 'idiot', - 'ignorant', - 'stupid', - 'vegas', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'person facepalming: medium skin tone', - char: '\u{1F926}\u{1F3FD}', - shortName: 'person_facepalming_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'disbelief', - 'exasperation', - 'face', - 'medium skin tone', - 'palm', - 'uc9', - 'diversity', - 'men', - 'stressed', - 'boys night', - 'facepalm', - 'dumb', - 'las vegas', - 'crazy', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'whoops', - 'oops', - 'mistake', - 'idiot', - 'ignorant', - 'stupid', - 'vegas', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'person facepalming: medium-dark skin tone', - char: '\u{1F926}\u{1F3FE}', - shortName: 'person_facepalming_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'disbelief', - 'exasperation', - 'face', - 'medium-dark skin tone', - 'palm', - 'uc9', - 'diversity', - 'men', - 'stressed', - 'boys night', - 'facepalm', - 'dumb', - 'las vegas', - 'crazy', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'whoops', - 'oops', - 'mistake', - 'idiot', - 'ignorant', - 'stupid', - 'vegas', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'person facepalming: dark skin tone', - char: '\u{1F926}\u{1F3FF}', - shortName: 'person_facepalming_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'dark skin tone', - 'disbelief', - 'exasperation', - 'face', - 'palm', - 'uc9', - 'diversity', - 'men', - 'stressed', - 'boys night', - 'facepalm', - 'dumb', - 'las vegas', - 'crazy', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'whoops', - 'oops', - 'mistake', - 'idiot', - 'ignorant', - 'stupid', - 'vegas', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'woman facepalming', - char: '\u{1F926}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_facepalming', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'disbelief', - 'exasperation', - 'facepalm', - 'woman', - 'uc9', - 'diversity', - 'women', - 'stressed', - 'facepalm', - 'dumb', - 'las vegas', - 'crazy', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'whoops', - 'oops', - 'mistake', - 'idiot', - 'ignorant', - 'stupid', - 'vegas', - 'weird', - 'awkward', - 'insane', - 'wild' - ]), - Emoji( - name: 'woman facepalming: light skin tone', - char: '\u{1F926}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_facepalming_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'disbelief', - 'exasperation', - 'facepalm', - 'light skin tone', - 'woman', - 'uc9', - 'diversity', - 'women', - 'stressed', - 'facepalm', - 'dumb', - 'las vegas', - 'crazy', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'whoops', - 'oops', - 'mistake', - 'idiot', - 'ignorant', - 'stupid', - 'vegas', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'woman facepalming: medium-light skin tone', - char: '\u{1F926}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_facepalming_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'disbelief', - 'exasperation', - 'facepalm', - 'medium-light skin tone', - 'woman', - 'uc9', - 'diversity', - 'women', - 'stressed', - 'facepalm', - 'dumb', - 'las vegas', - 'crazy', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'whoops', - 'oops', - 'mistake', - 'idiot', - 'ignorant', - 'stupid', - 'vegas', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'woman facepalming: medium skin tone', - char: '\u{1F926}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_facepalming_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'disbelief', - 'exasperation', - 'facepalm', - 'medium skin tone', - 'woman', - 'uc9', - 'diversity', - 'women', - 'stressed', - 'facepalm', - 'dumb', - 'las vegas', - 'crazy', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'whoops', - 'oops', - 'mistake', - 'idiot', - 'ignorant', - 'stupid', - 'vegas', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'woman facepalming: medium-dark skin tone', - char: '\u{1F926}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_facepalming_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'disbelief', - 'exasperation', - 'facepalm', - 'medium-dark skin tone', - 'woman', - 'uc9', - 'diversity', - 'women', - 'stressed', - 'facepalm', - 'dumb', - 'las vegas', - 'crazy', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'whoops', - 'oops', - 'mistake', - 'idiot', - 'ignorant', - 'stupid', - 'vegas', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'woman facepalming: dark skin tone', - char: '\u{1F926}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_facepalming_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'dark skin tone', - 'disbelief', - 'exasperation', - 'facepalm', - 'woman', - 'uc9', - 'diversity', - 'women', - 'stressed', - 'facepalm', - 'dumb', - 'las vegas', - 'crazy', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'whoops', - 'oops', - 'mistake', - 'idiot', - 'ignorant', - 'stupid', - 'vegas', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'man facepalming', - char: '\u{1F926}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_facepalming', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'disbelief', - 'exasperation', - 'facepalm', - 'man', - 'uc9', - 'diversity', - 'men', - 'stressed', - 'boys night', - 'facepalm', - 'dumb', - 'las vegas', - 'crazy', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'whoops', - 'oops', - 'mistake', - 'idiot', - 'ignorant', - 'stupid', - 'vegas', - 'weird', - 'awkward', - 'insane', - 'wild' - ]), - Emoji( - name: 'man facepalming: light skin tone', - char: '\u{1F926}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_facepalming_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'disbelief', - 'exasperation', - 'facepalm', - 'light skin tone', - 'man', - 'uc9', - 'diversity', - 'men', - 'stressed', - 'boys night', - 'facepalm', - 'dumb', - 'las vegas', - 'crazy', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'whoops', - 'oops', - 'mistake', - 'idiot', - 'ignorant', - 'stupid', - 'vegas', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'man facepalming: medium-light skin tone', - char: '\u{1F926}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_facepalming_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'disbelief', - 'exasperation', - 'facepalm', - 'man', - 'medium-light skin tone', - 'uc9', - 'diversity', - 'men', - 'stressed', - 'boys night', - 'facepalm', - 'dumb', - 'las vegas', - 'crazy', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'whoops', - 'oops', - 'mistake', - 'idiot', - 'ignorant', - 'stupid', - 'vegas', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'man facepalming: medium skin tone', - char: '\u{1F926}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_facepalming_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'disbelief', - 'exasperation', - 'facepalm', - 'man', - 'medium skin tone', - 'uc9', - 'diversity', - 'men', - 'stressed', - 'boys night', - 'facepalm', - 'dumb', - 'las vegas', - 'crazy', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'whoops', - 'oops', - 'mistake', - 'idiot', - 'ignorant', - 'stupid', - 'vegas', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'man facepalming: medium-dark skin tone', - char: '\u{1F926}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_facepalming_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'disbelief', - 'exasperation', - 'facepalm', - 'man', - 'medium-dark skin tone', - 'uc9', - 'diversity', - 'men', - 'stressed', - 'boys night', - 'facepalm', - 'dumb', - 'las vegas', - 'crazy', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'whoops', - 'oops', - 'mistake', - 'idiot', - 'ignorant', - 'stupid', - 'vegas', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'man facepalming: dark skin tone', - char: '\u{1F926}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_facepalming_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'dark skin tone', - 'disbelief', - 'exasperation', - 'facepalm', - 'man', - 'uc9', - 'diversity', - 'men', - 'stressed', - 'boys night', - 'facepalm', - 'dumb', - 'las vegas', - 'crazy', - 'wrong', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'whoops', - 'oops', - 'mistake', - 'idiot', - 'ignorant', - 'stupid', - 'vegas', - 'weird', - 'awkward', - 'insane', - 'wild' - ], - modifiable: true), - Emoji( - name: 'person shrugging', - char: '\u{1F937}', - shortName: 'person_shrugging', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'doubt', - 'ignorance', - 'indifference', - 'shrug', - 'uc9', - 'diversity', - 'men', - 'shrug', - 'neutral', - 'bitch', - 'daddy', - 'doubt', - 'dumb', - 'confused', - 'what', - 'crazy', - 'mystery', - 'question', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'puta', - 'pute', - 'dad', - 'papa', - 'pere', - 'father', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical', - 'idiot', - 'ignorant', - 'stupid', - 'perplexed', - 'weird', - 'awkward', - 'insane', - 'wild', - 'quiz', - 'puzzled' - ]), - Emoji( - name: 'person shrugging: light skin tone', - char: '\u{1F937}\u{1F3FB}', - shortName: 'person_shrugging_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'doubt', - 'ignorance', - 'indifference', - 'light skin tone', - 'shrug', - 'uc9', - 'diversity', - 'men', - 'shrug', - 'neutral', - 'bitch', - 'daddy', - 'doubt', - 'dumb', - 'confused', - 'what', - 'crazy', - 'mystery', - 'question', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'puta', - 'pute', - 'dad', - 'papa', - 'pere', - 'father', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical', - 'idiot', - 'ignorant', - 'stupid', - 'perplexed', - 'weird', - 'awkward', - 'insane', - 'wild', - 'quiz', - 'puzzled' - ], - modifiable: true), - Emoji( - name: 'person shrugging: medium-light skin tone', - char: '\u{1F937}\u{1F3FC}', - shortName: 'person_shrugging_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'doubt', - 'ignorance', - 'indifference', - 'medium-light skin tone', - 'shrug', - 'uc9', - 'diversity', - 'men', - 'shrug', - 'neutral', - 'bitch', - 'daddy', - 'doubt', - 'dumb', - 'confused', - 'what', - 'crazy', - 'mystery', - 'question', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'puta', - 'pute', - 'dad', - 'papa', - 'pere', - 'father', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical', - 'idiot', - 'ignorant', - 'stupid', - 'perplexed', - 'weird', - 'awkward', - 'insane', - 'wild', - 'quiz', - 'puzzled' - ], - modifiable: true), - Emoji( - name: 'person shrugging: medium skin tone', - char: '\u{1F937}\u{1F3FD}', - shortName: 'person_shrugging_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'doubt', - 'ignorance', - 'indifference', - 'medium skin tone', - 'shrug', - 'uc9', - 'diversity', - 'men', - 'shrug', - 'neutral', - 'bitch', - 'daddy', - 'doubt', - 'dumb', - 'confused', - 'what', - 'crazy', - 'mystery', - 'question', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'puta', - 'pute', - 'dad', - 'papa', - 'pere', - 'father', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical', - 'idiot', - 'ignorant', - 'stupid', - 'perplexed', - 'weird', - 'awkward', - 'insane', - 'wild', - 'quiz', - 'puzzled' - ], - modifiable: true), - Emoji( - name: 'person shrugging: medium-dark skin tone', - char: '\u{1F937}\u{1F3FE}', - shortName: 'person_shrugging_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'doubt', - 'ignorance', - 'indifference', - 'medium-dark skin tone', - 'shrug', - 'uc9', - 'diversity', - 'men', - 'shrug', - 'neutral', - 'bitch', - 'daddy', - 'doubt', - 'dumb', - 'confused', - 'what', - 'crazy', - 'mystery', - 'question', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'puta', - 'pute', - 'dad', - 'papa', - 'pere', - 'father', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical', - 'idiot', - 'ignorant', - 'stupid', - 'perplexed', - 'weird', - 'awkward', - 'insane', - 'wild', - 'quiz', - 'puzzled' - ], - modifiable: true), - Emoji( - name: 'person shrugging: dark skin tone', - char: '\u{1F937}\u{1F3FF}', - shortName: 'person_shrugging_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'dark skin tone', - 'doubt', - 'ignorance', - 'indifference', - 'shrug', - 'uc9', - 'diversity', - 'men', - 'shrug', - 'neutral', - 'bitch', - 'daddy', - 'doubt', - 'dumb', - 'confused', - 'what', - 'crazy', - 'mystery', - 'question', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'puta', - 'pute', - 'dad', - 'papa', - 'pere', - 'father', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical', - 'idiot', - 'ignorant', - 'stupid', - 'perplexed', - 'weird', - 'awkward', - 'insane', - 'wild', - 'quiz', - 'puzzled' - ], - modifiable: true), - Emoji( - name: 'woman shrugging', - char: '\u{1F937}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_shrugging', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'doubt', - 'ignorance', - 'indifference', - 'shrug', - 'woman', - 'uc9', - 'diversity', - 'women', - 'shrug', - 'neutral', - 'bitch', - 'doubt', - 'dumb', - 'guilty', - 'confused', - 'what', - 'crazy', - 'mystery', - 'question', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'puta', - 'pute', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical', - 'idiot', - 'ignorant', - 'stupid', - 'perplexed', - 'weird', - 'awkward', - 'insane', - 'wild', - 'quiz', - 'puzzled' - ]), - Emoji( - name: 'woman shrugging: light skin tone', - char: '\u{1F937}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_shrugging_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'doubt', - 'ignorance', - 'indifference', - 'light skin tone', - 'shrug', - 'woman', - 'uc9', - 'diversity', - 'women', - 'shrug', - 'neutral', - 'bitch', - 'doubt', - 'dumb', - 'guilty', - 'confused', - 'what', - 'crazy', - 'mystery', - 'question', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'puta', - 'pute', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical', - 'idiot', - 'ignorant', - 'stupid', - 'perplexed', - 'weird', - 'awkward', - 'insane', - 'wild', - 'quiz', - 'puzzled' - ], - modifiable: true), - Emoji( - name: 'woman shrugging: medium-light skin tone', - char: '\u{1F937}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_shrugging_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'doubt', - 'ignorance', - 'indifference', - 'medium-light skin tone', - 'shrug', - 'woman', - 'uc9', - 'diversity', - 'women', - 'shrug', - 'neutral', - 'bitch', - 'doubt', - 'dumb', - 'guilty', - 'confused', - 'what', - 'crazy', - 'mystery', - 'question', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'puta', - 'pute', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical', - 'idiot', - 'ignorant', - 'stupid', - 'perplexed', - 'weird', - 'awkward', - 'insane', - 'wild', - 'quiz', - 'puzzled' - ], - modifiable: true), - Emoji( - name: 'woman shrugging: medium skin tone', - char: '\u{1F937}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_shrugging_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'doubt', - 'ignorance', - 'indifference', - 'medium skin tone', - 'shrug', - 'woman', - 'uc9', - 'diversity', - 'women', - 'shrug', - 'neutral', - 'bitch', - 'doubt', - 'dumb', - 'guilty', - 'confused', - 'what', - 'crazy', - 'mystery', - 'question', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'puta', - 'pute', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical', - 'idiot', - 'ignorant', - 'stupid', - 'perplexed', - 'weird', - 'awkward', - 'insane', - 'wild', - 'quiz', - 'puzzled' - ], - modifiable: true), - Emoji( - name: 'woman shrugging: medium-dark skin tone', - char: '\u{1F937}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_shrugging_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'doubt', - 'ignorance', - 'indifference', - 'medium-dark skin tone', - 'shrug', - 'woman', - 'uc9', - 'diversity', - 'women', - 'shrug', - 'neutral', - 'bitch', - 'doubt', - 'dumb', - 'guilty', - 'confused', - 'what', - 'crazy', - 'mystery', - 'question', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'puta', - 'pute', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical', - 'idiot', - 'ignorant', - 'stupid', - 'perplexed', - 'weird', - 'awkward', - 'insane', - 'wild', - 'quiz', - 'puzzled' - ], - modifiable: true), - Emoji( - name: 'woman shrugging: dark skin tone', - char: '\u{1F937}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_shrugging_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'dark skin tone', - 'doubt', - 'ignorance', - 'indifference', - 'shrug', - 'woman', - 'uc9', - 'diversity', - 'women', - 'shrug', - 'neutral', - 'bitch', - 'doubt', - 'dumb', - 'guilty', - 'confused', - 'what', - 'crazy', - 'mystery', - 'question', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'puta', - 'pute', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical', - 'idiot', - 'ignorant', - 'stupid', - 'perplexed', - 'weird', - 'awkward', - 'insane', - 'wild', - 'quiz', - 'puzzled' - ], - modifiable: true), - Emoji( - name: 'man shrugging', - char: '\u{1F937}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_shrugging', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'doubt', - 'ignorance', - 'indifference', - 'man', - 'shrug', - 'uc9', - 'diversity', - 'men', - 'shrug', - 'neutral', - 'daddy', - 'doubt', - 'dumb', - 'guilty', - 'confused', - 'what', - 'crazy', - 'mystery', - 'question', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'dad', - 'papa', - 'pere', - 'father', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical', - 'idiot', - 'ignorant', - 'stupid', - 'perplexed', - 'weird', - 'awkward', - 'insane', - 'wild', - 'quiz', - 'puzzled' - ]), - Emoji( - name: 'man shrugging: light skin tone', - char: '\u{1F937}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_shrugging_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'doubt', - 'ignorance', - 'indifference', - 'light skin tone', - 'man', - 'shrug', - 'uc9', - 'diversity', - 'men', - 'shrug', - 'neutral', - 'daddy', - 'doubt', - 'dumb', - 'guilty', - 'confused', - 'what', - 'crazy', - 'mystery', - 'question', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'dad', - 'papa', - 'pere', - 'father', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical', - 'idiot', - 'ignorant', - 'stupid', - 'perplexed', - 'weird', - 'awkward', - 'insane', - 'wild', - 'quiz', - 'puzzled' - ], - modifiable: true), - Emoji( - name: 'man shrugging: medium-light skin tone', - char: '\u{1F937}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_shrugging_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'doubt', - 'ignorance', - 'indifference', - 'man', - 'medium-light skin tone', - 'shrug', - 'uc9', - 'diversity', - 'men', - 'shrug', - 'neutral', - 'daddy', - 'doubt', - 'dumb', - 'guilty', - 'confused', - 'what', - 'crazy', - 'mystery', - 'question', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'dad', - 'papa', - 'pere', - 'father', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical', - 'idiot', - 'ignorant', - 'stupid', - 'perplexed', - 'weird', - 'awkward', - 'insane', - 'wild', - 'quiz', - 'puzzled' - ], - modifiable: true), - Emoji( - name: 'man shrugging: medium skin tone', - char: '\u{1F937}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_shrugging_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'doubt', - 'ignorance', - 'indifference', - 'man', - 'medium skin tone', - 'shrug', - 'uc9', - 'diversity', - 'men', - 'shrug', - 'neutral', - 'daddy', - 'doubt', - 'dumb', - 'guilty', - 'confused', - 'what', - 'crazy', - 'mystery', - 'question', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'dad', - 'papa', - 'pere', - 'father', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical', - 'idiot', - 'ignorant', - 'stupid', - 'perplexed', - 'weird', - 'awkward', - 'insane', - 'wild', - 'quiz', - 'puzzled' - ], - modifiable: true), - Emoji( - name: 'man shrugging: medium-dark skin tone', - char: '\u{1F937}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_shrugging_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'doubt', - 'ignorance', - 'indifference', - 'man', - 'medium-dark skin tone', - 'shrug', - 'uc9', - 'diversity', - 'men', - 'shrug', - 'neutral', - 'daddy', - 'doubt', - 'dumb', - 'guilty', - 'confused', - 'what', - 'crazy', - 'mystery', - 'question', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'dad', - 'papa', - 'pere', - 'father', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical', - 'idiot', - 'ignorant', - 'stupid', - 'perplexed', - 'weird', - 'awkward', - 'insane', - 'wild', - 'quiz', - 'puzzled' - ], - modifiable: true), - Emoji( - name: 'man shrugging: dark skin tone', - char: '\u{1F937}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_shrugging_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'dark skin tone', - 'doubt', - 'ignorance', - 'indifference', - 'man', - 'shrug', - 'uc9', - 'diversity', - 'men', - 'shrug', - 'neutral', - 'daddy', - 'doubt', - 'dumb', - 'guilty', - 'confused', - 'what', - 'crazy', - 'mystery', - 'question', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'dad', - 'papa', - 'pere', - 'father', - 'unsure', - 'thinking', - 'wonder', - 'curious', - 'worry', - 'pensive', - 'remember', - 'skeptical', - 'idiot', - 'ignorant', - 'stupid', - 'perplexed', - 'weird', - 'awkward', - 'insane', - 'wild', - 'quiz', - 'puzzled' - ], - modifiable: true), - Emoji( - name: 'person pouting', - char: '\u{1F64E}', - shortName: 'person_pouting', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'pouting', - 'uc6', - 'diversity', - 'sad', - 'men', - 'angry', - 'stressed', - 'hate', - 'bitch', - 'daddy', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'puta', - 'pute', - 'dad', - 'papa', - 'pere', - 'father', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'person pouting: light skin tone', - char: '\u{1F64E}\u{1F3FB}', - shortName: 'person_pouting_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'light skin tone', - 'pouting', - 'uc8', - 'diversity', - 'sad', - 'men', - 'angry', - 'stressed', - 'hate', - 'bitch', - 'daddy', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'puta', - 'pute', - 'dad', - 'papa', - 'pere', - 'father', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person pouting: medium-light skin tone', - char: '\u{1F64E}\u{1F3FC}', - shortName: 'person_pouting_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'medium-light skin tone', - 'pouting', - 'uc8', - 'diversity', - 'sad', - 'men', - 'angry', - 'stressed', - 'hate', - 'bitch', - 'daddy', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'puta', - 'pute', - 'dad', - 'papa', - 'pere', - 'father', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person pouting: medium skin tone', - char: '\u{1F64E}\u{1F3FD}', - shortName: 'person_pouting_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'medium skin tone', - 'pouting', - 'uc8', - 'diversity', - 'sad', - 'men', - 'angry', - 'stressed', - 'hate', - 'bitch', - 'daddy', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'puta', - 'pute', - 'dad', - 'papa', - 'pere', - 'father', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person pouting: medium-dark skin tone', - char: '\u{1F64E}\u{1F3FE}', - shortName: 'person_pouting_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'medium-dark skin tone', - 'pouting', - 'uc8', - 'diversity', - 'sad', - 'men', - 'angry', - 'stressed', - 'hate', - 'bitch', - 'daddy', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'puta', - 'pute', - 'dad', - 'papa', - 'pere', - 'father', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person pouting: dark skin tone', - char: '\u{1F64E}\u{1F3FF}', - shortName: 'person_pouting_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'dark skin tone', - 'gesture', - 'pouting', - 'uc8', - 'diversity', - 'sad', - 'men', - 'angry', - 'stressed', - 'hate', - 'bitch', - 'daddy', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'puta', - 'pute', - 'dad', - 'papa', - 'pere', - 'father', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman pouting', - char: '\u{1F64E}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_pouting', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'pouting', - 'woman', - 'uc6', - 'diversity', - 'sad', - 'women', - 'stressed', - 'bitch', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'woman', - 'female', - 'puta', - 'pute', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'woman pouting: light skin tone', - char: '\u{1F64E}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_pouting_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'light skin tone', - 'pouting', - 'woman', - 'uc8', - 'diversity', - 'sad', - 'women', - 'stressed', - 'bitch', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'woman', - 'female', - 'puta', - 'pute', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman pouting: medium-light skin tone', - char: '\u{1F64E}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_pouting_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'medium-light skin tone', - 'pouting', - 'woman', - 'uc8', - 'diversity', - 'sad', - 'women', - 'stressed', - 'bitch', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'woman', - 'female', - 'puta', - 'pute', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman pouting: medium skin tone', - char: '\u{1F64E}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_pouting_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'medium skin tone', - 'pouting', - 'woman', - 'uc8', - 'diversity', - 'sad', - 'women', - 'stressed', - 'bitch', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'woman', - 'female', - 'puta', - 'pute', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman pouting: medium-dark skin tone', - char: '\u{1F64E}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_pouting_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'medium-dark skin tone', - 'pouting', - 'woman', - 'uc8', - 'diversity', - 'sad', - 'women', - 'stressed', - 'bitch', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'woman', - 'female', - 'puta', - 'pute', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman pouting: dark skin tone', - char: '\u{1F64E}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_pouting_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'dark skin tone', - 'gesture', - 'pouting', - 'woman', - 'uc8', - 'diversity', - 'sad', - 'women', - 'stressed', - 'bitch', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'woman', - 'female', - 'puta', - 'pute', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'man pouting', - char: '\u{1F64E}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_pouting', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'man', - 'pouting', - 'uc6', - 'diversity', - 'sad', - 'men', - 'angry', - 'stressed', - 'hate', - 'daddy', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'dad', - 'papa', - 'pere', - 'father' - ]), - Emoji( - name: 'man pouting: light skin tone', - char: '\u{1F64E}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_pouting_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'light skin tone', - 'man', - 'pouting', - 'uc8', - 'diversity', - 'sad', - 'men', - 'angry', - 'stressed', - 'hate', - 'daddy', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'dad', - 'papa', - 'pere', - 'father' - ], - modifiable: true), - Emoji( - name: 'man pouting: medium-light skin tone', - char: '\u{1F64E}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_pouting_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'man', - 'medium-light skin tone', - 'pouting', - 'uc8', - 'diversity', - 'sad', - 'men', - 'angry', - 'stressed', - 'hate', - 'daddy', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'dad', - 'papa', - 'pere', - 'father' - ], - modifiable: true), - Emoji( - name: 'man pouting: medium skin tone', - char: '\u{1F64E}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_pouting_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'man', - 'medium skin tone', - 'pouting', - 'uc8', - 'diversity', - 'sad', - 'men', - 'angry', - 'stressed', - 'hate', - 'daddy', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'dad', - 'papa', - 'pere', - 'father' - ], - modifiable: true), - Emoji( - name: 'man pouting: medium-dark skin tone', - char: '\u{1F64E}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_pouting_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'gesture', - 'man', - 'medium-dark skin tone', - 'pouting', - 'uc8', - 'diversity', - 'sad', - 'men', - 'angry', - 'stressed', - 'hate', - 'daddy', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'dad', - 'papa', - 'pere', - 'father' - ], - modifiable: true), - Emoji( - name: 'man pouting: dark skin tone', - char: '\u{1F64E}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_pouting_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'dark skin tone', - 'gesture', - 'man', - 'pouting', - 'uc8', - 'diversity', - 'sad', - 'men', - 'angry', - 'stressed', - 'hate', - 'daddy', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'dad', - 'papa', - 'pere', - 'father' - ], - modifiable: true), - Emoji( - name: 'person frowning', - char: '\u{1F64D}', - shortName: 'person_frowning', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'frown', - 'gesture', - 'uc6', - 'diversity', - 'sad', - 'men', - 'angry', - 'stressed', - 'hate', - 'bitch', - 'daddy', - 'husband', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'puta', - 'pute', - 'dad', - 'papa', - 'pere', - 'father', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'person frowning: light skin tone', - char: '\u{1F64D}\u{1F3FB}', - shortName: 'person_frowning_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'frown', - 'gesture', - 'light skin tone', - 'uc8', - 'diversity', - 'sad', - 'men', - 'angry', - 'stressed', - 'hate', - 'bitch', - 'daddy', - 'husband', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'puta', - 'pute', - 'dad', - 'papa', - 'pere', - 'father', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person frowning: medium-light skin tone', - char: '\u{1F64D}\u{1F3FC}', - shortName: 'person_frowning_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'frown', - 'gesture', - 'medium-light skin tone', - 'uc8', - 'diversity', - 'sad', - 'men', - 'angry', - 'stressed', - 'hate', - 'bitch', - 'daddy', - 'husband', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'puta', - 'pute', - 'dad', - 'papa', - 'pere', - 'father', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person frowning: medium skin tone', - char: '\u{1F64D}\u{1F3FD}', - shortName: 'person_frowning_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'frown', - 'gesture', - 'medium skin tone', - 'uc8', - 'diversity', - 'sad', - 'men', - 'angry', - 'stressed', - 'hate', - 'bitch', - 'daddy', - 'husband', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'puta', - 'pute', - 'dad', - 'papa', - 'pere', - 'father', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person frowning: medium-dark skin tone', - char: '\u{1F64D}\u{1F3FE}', - shortName: 'person_frowning_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'frown', - 'gesture', - 'medium-dark skin tone', - 'uc8', - 'diversity', - 'sad', - 'men', - 'angry', - 'stressed', - 'hate', - 'bitch', - 'daddy', - 'husband', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'puta', - 'pute', - 'dad', - 'papa', - 'pere', - 'father', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person frowning: dark skin tone', - char: '\u{1F64D}\u{1F3FF}', - shortName: 'person_frowning_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'dark skin tone', - 'frown', - 'gesture', - 'uc8', - 'diversity', - 'sad', - 'men', - 'angry', - 'stressed', - 'hate', - 'bitch', - 'daddy', - 'husband', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'puta', - 'pute', - 'dad', - 'papa', - 'pere', - 'father', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman frowning', - char: '\u{1F64D}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_frowning', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'frowning', - 'gesture', - 'woman', - 'uc6', - 'diversity', - 'sad', - 'women', - 'stressed', - 'bitch', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'woman', - 'female', - 'puta', - 'pute', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'woman frowning: light skin tone', - char: '\u{1F64D}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_frowning_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'frowning', - 'gesture', - 'light skin tone', - 'woman', - 'uc8', - 'diversity', - 'sad', - 'women', - 'stressed', - 'bitch', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'woman', - 'female', - 'puta', - 'pute', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman frowning: medium-light skin tone', - char: '\u{1F64D}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_frowning_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'frowning', - 'gesture', - 'medium-light skin tone', - 'woman', - 'uc8', - 'diversity', - 'sad', - 'women', - 'stressed', - 'bitch', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'woman', - 'female', - 'puta', - 'pute', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman frowning: medium skin tone', - char: '\u{1F64D}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_frowning_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'frowning', - 'gesture', - 'medium skin tone', - 'woman', - 'uc8', - 'diversity', - 'sad', - 'women', - 'stressed', - 'bitch', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'woman', - 'female', - 'puta', - 'pute', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman frowning: medium-dark skin tone', - char: '\u{1F64D}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_frowning_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'frowning', - 'gesture', - 'medium-dark skin tone', - 'woman', - 'uc8', - 'diversity', - 'sad', - 'women', - 'stressed', - 'bitch', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'woman', - 'female', - 'puta', - 'pute', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman frowning: dark skin tone', - char: '\u{1F64D}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_frowning_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'dark skin tone', - 'frowning', - 'gesture', - 'woman', - 'uc8', - 'diversity', - 'sad', - 'women', - 'stressed', - 'bitch', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'woman', - 'female', - 'puta', - 'pute', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'man frowning', - char: '\u{1F64D}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_frowning', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'frowning', - 'gesture', - 'man', - 'uc6', - 'diversity', - 'sad', - 'men', - 'angry', - 'stressed', - 'hate', - 'daddy', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'dad', - 'papa', - 'pere', - 'father' - ]), - Emoji( - name: 'man frowning: light skin tone', - char: '\u{1F64D}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_frowning_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'frowning', - 'gesture', - 'light skin tone', - 'man', - 'uc8', - 'diversity', - 'sad', - 'men', - 'angry', - 'stressed', - 'hate', - 'daddy', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'dad', - 'papa', - 'pere', - 'father' - ], - modifiable: true), - Emoji( - name: 'man frowning: medium-light skin tone', - char: '\u{1F64D}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_frowning_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'frowning', - 'gesture', - 'man', - 'medium-light skin tone', - 'uc8', - 'diversity', - 'sad', - 'men', - 'angry', - 'stressed', - 'hate', - 'daddy', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'dad', - 'papa', - 'pere', - 'father' - ], - modifiable: true), - Emoji( - name: 'man frowning: medium skin tone', - char: '\u{1F64D}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_frowning_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'frowning', - 'gesture', - 'man', - 'medium skin tone', - 'uc8', - 'diversity', - 'sad', - 'men', - 'angry', - 'stressed', - 'hate', - 'daddy', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'dad', - 'papa', - 'pere', - 'father' - ], - modifiable: true), - Emoji( - name: 'man frowning: medium-dark skin tone', - char: '\u{1F64D}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_frowning_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'frowning', - 'gesture', - 'man', - 'medium-dark skin tone', - 'uc8', - 'diversity', - 'sad', - 'men', - 'angry', - 'stressed', - 'hate', - 'daddy', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'dad', - 'papa', - 'pere', - 'father' - ], - modifiable: true), - Emoji( - name: 'man frowning: dark skin tone', - char: '\u{1F64D}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_frowning_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personGesture, - keywords: [ - 'dark skin tone', - 'frowning', - 'gesture', - 'man', - 'uc8', - 'diversity', - 'sad', - 'men', - 'angry', - 'stressed', - 'hate', - 'daddy', - 'husband', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'triste', - 'depression', - 'negative', - 'sadness', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'dad', - 'papa', - 'pere', - 'father' - ], - modifiable: true), - Emoji( - name: 'person getting haircut', - char: '\u{1F487}', - shortName: 'person_getting_haircut', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'barber', - 'beauty', - 'haircut', - 'parlor', - 'uc6', - 'diversity', - 'men', - 'daddy', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'dad', - 'papa', - 'pere', - 'father', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'person getting haircut: light skin tone', - char: '\u{1F487}\u{1F3FB}', - shortName: 'person_getting_haircut_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'barber', - 'beauty', - 'haircut', - 'light skin tone', - 'parlor', - 'uc8', - 'diversity', - 'men', - 'daddy', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'dad', - 'papa', - 'pere', - 'father', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person getting haircut: medium-light skin tone', - char: '\u{1F487}\u{1F3FC}', - shortName: 'person_getting_haircut_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'barber', - 'beauty', - 'haircut', - 'medium-light skin tone', - 'parlor', - 'uc8', - 'diversity', - 'men', - 'daddy', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'dad', - 'papa', - 'pere', - 'father', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person getting haircut: medium skin tone', - char: '\u{1F487}\u{1F3FD}', - shortName: 'person_getting_haircut_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'barber', - 'beauty', - 'haircut', - 'medium skin tone', - 'parlor', - 'uc8', - 'diversity', - 'men', - 'daddy', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'dad', - 'papa', - 'pere', - 'father', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person getting haircut: medium-dark skin tone', - char: '\u{1F487}\u{1F3FE}', - shortName: 'person_getting_haircut_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'barber', - 'beauty', - 'haircut', - 'medium-dark skin tone', - 'parlor', - 'uc8', - 'diversity', - 'men', - 'daddy', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'dad', - 'papa', - 'pere', - 'father', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person getting haircut: dark skin tone', - char: '\u{1F487}\u{1F3FF}', - shortName: 'person_getting_haircut_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'barber', - 'beauty', - 'dark skin tone', - 'haircut', - 'parlor', - 'uc8', - 'diversity', - 'men', - 'daddy', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'dad', - 'papa', - 'pere', - 'father', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman getting haircut', - char: '\u{1F487}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_getting_haircut', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'haircut', - 'woman', - 'uc6', - 'diversity', - 'women', - 'girls night', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'ladies night', - 'girls only', - 'girlfriend', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'woman getting haircut: light skin tone', - char: '\u{1F487}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_getting_haircut_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'haircut', - 'light skin tone', - 'woman', - 'uc8', - 'diversity', - 'women', - 'girls night', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'ladies night', - 'girls only', - 'girlfriend', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman getting haircut: medium-light skin tone', - char: '\u{1F487}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_getting_haircut_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'haircut', - 'medium-light skin tone', - 'woman', - 'uc8', - 'diversity', - 'women', - 'girls night', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'ladies night', - 'girls only', - 'girlfriend', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman getting haircut: medium skin tone', - char: '\u{1F487}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_getting_haircut_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'haircut', - 'medium skin tone', - 'woman', - 'uc8', - 'diversity', - 'women', - 'girls night', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'ladies night', - 'girls only', - 'girlfriend', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman getting haircut: medium-dark skin tone', - char: '\u{1F487}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_getting_haircut_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'haircut', - 'medium-dark skin tone', - 'woman', - 'uc8', - 'diversity', - 'women', - 'girls night', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'ladies night', - 'girls only', - 'girlfriend', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman getting haircut: dark skin tone', - char: '\u{1F487}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_getting_haircut_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dark skin tone', - 'haircut', - 'woman', - 'uc8', - 'diversity', - 'women', - 'girls night', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'ladies night', - 'girls only', - 'girlfriend', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'man getting haircut', - char: '\u{1F487}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_getting_haircut', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'haircut', - 'man', - 'uc6', - 'diversity', - 'men', - 'daddy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'dad', - 'papa', - 'pere', - 'father' - ]), - Emoji( - name: 'man getting haircut: light skin tone', - char: '\u{1F487}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_getting_haircut_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'haircut', - 'light skin tone', - 'man', - 'uc8', - 'diversity', - 'men', - 'daddy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'dad', - 'papa', - 'pere', - 'father' - ], - modifiable: true), - Emoji( - name: 'man getting haircut: medium-light skin tone', - char: '\u{1F487}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_getting_haircut_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'haircut', - 'man', - 'medium-light skin tone', - 'uc8', - 'diversity', - 'men', - 'daddy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'dad', - 'papa', - 'pere', - 'father' - ], - modifiable: true), - Emoji( - name: 'man getting haircut: medium skin tone', - char: '\u{1F487}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_getting_haircut_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'haircut', - 'man', - 'medium skin tone', - 'uc8', - 'diversity', - 'men', - 'daddy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'dad', - 'papa', - 'pere', - 'father' - ], - modifiable: true), - Emoji( - name: 'man getting haircut: medium-dark skin tone', - char: '\u{1F487}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_getting_haircut_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'haircut', - 'man', - 'medium-dark skin tone', - 'uc8', - 'diversity', - 'men', - 'daddy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'dad', - 'papa', - 'pere', - 'father' - ], - modifiable: true), - Emoji( - name: 'man getting haircut: dark skin tone', - char: '\u{1F487}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_getting_haircut_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dark skin tone', - 'haircut', - 'man', - 'uc8', - 'diversity', - 'men', - 'daddy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'dad', - 'papa', - 'pere', - 'father' - ], - modifiable: true), - Emoji( - name: 'person getting massage', - char: '\u{1F486}', - shortName: 'person_getting_massage', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'face', - 'massage', - 'salon', - 'uc6', - 'diversity', - 'men', - 'girls night', - 'pleased', - 'yoga', - 'calm', - 'spa', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'ladies night', - 'girls only', - 'girlfriend', - 'please', - 'chill', - 'confident', - 'content', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'person getting massage: light skin tone', - char: '\u{1F486}\u{1F3FB}', - shortName: 'person_getting_massage_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'face', - 'light skin tone', - 'massage', - 'salon', - 'uc8', - 'diversity', - 'men', - 'girls night', - 'pleased', - 'yoga', - 'calm', - 'spa', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'ladies night', - 'girls only', - 'girlfriend', - 'please', - 'chill', - 'confident', - 'content', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person getting massage: medium-light skin tone', - char: '\u{1F486}\u{1F3FC}', - shortName: 'person_getting_massage_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'face', - 'massage', - 'medium-light skin tone', - 'salon', - 'uc8', - 'diversity', - 'men', - 'girls night', - 'pleased', - 'yoga', - 'calm', - 'spa', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'ladies night', - 'girls only', - 'girlfriend', - 'please', - 'chill', - 'confident', - 'content', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person getting massage: medium skin tone', - char: '\u{1F486}\u{1F3FD}', - shortName: 'person_getting_massage_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'face', - 'massage', - 'medium skin tone', - 'salon', - 'uc8', - 'diversity', - 'men', - 'girls night', - 'pleased', - 'yoga', - 'calm', - 'spa', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'ladies night', - 'girls only', - 'girlfriend', - 'please', - 'chill', - 'confident', - 'content', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person getting massage: medium-dark skin tone', - char: '\u{1F486}\u{1F3FE}', - shortName: 'person_getting_massage_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'face', - 'massage', - 'medium-dark skin tone', - 'salon', - 'uc8', - 'diversity', - 'men', - 'girls night', - 'pleased', - 'yoga', - 'calm', - 'spa', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'ladies night', - 'girls only', - 'girlfriend', - 'please', - 'chill', - 'confident', - 'content', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person getting massage: dark skin tone', - char: '\u{1F486}\u{1F3FF}', - shortName: 'person_getting_massage_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dark skin tone', - 'face', - 'massage', - 'salon', - 'uc8', - 'diversity', - 'men', - 'girls night', - 'pleased', - 'yoga', - 'calm', - 'spa', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'ladies night', - 'girls only', - 'girlfriend', - 'please', - 'chill', - 'confident', - 'content', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman getting massage', - char: '\u{1F486}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_getting_face_massage', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'face', - 'massage', - 'woman', - 'uc6', - 'diversity', - 'women', - 'girls night', - 'yoga', - 'calm', - 'spa', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'ladies night', - 'girls only', - 'girlfriend', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'woman getting massage: light skin tone', - char: '\u{1F486}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_getting_face_massage_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'face', - 'light skin tone', - 'massage', - 'woman', - 'uc8', - 'diversity', - 'women', - 'girls night', - 'yoga', - 'calm', - 'spa', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'ladies night', - 'girls only', - 'girlfriend', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman getting massage: medium-light skin tone', - char: '\u{1F486}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_getting_face_massage_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'face', - 'massage', - 'medium-light skin tone', - 'woman', - 'uc8', - 'diversity', - 'women', - 'girls night', - 'yoga', - 'calm', - 'spa', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'ladies night', - 'girls only', - 'girlfriend', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman getting massage: medium skin tone', - char: '\u{1F486}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_getting_face_massage_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'face', - 'massage', - 'medium skin tone', - 'woman', - 'uc8', - 'diversity', - 'women', - 'girls night', - 'yoga', - 'calm', - 'spa', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'ladies night', - 'girls only', - 'girlfriend', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman getting massage: medium-dark skin tone', - char: '\u{1F486}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_getting_face_massage_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'face', - 'massage', - 'medium-dark skin tone', - 'woman', - 'uc8', - 'diversity', - 'women', - 'girls night', - 'yoga', - 'calm', - 'spa', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'ladies night', - 'girls only', - 'girlfriend', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman getting massage: dark skin tone', - char: '\u{1F486}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_getting_face_massage_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dark skin tone', - 'face', - 'massage', - 'woman', - 'uc8', - 'diversity', - 'women', - 'girls night', - 'yoga', - 'calm', - 'spa', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'ladies night', - 'girls only', - 'girlfriend', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'man getting massage', - char: '\u{1F486}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_getting_face_massage', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'face', - 'man', - 'massage', - 'uc6', - 'diversity', - 'men', - 'yoga', - 'calm', - 'spa', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna' - ]), - Emoji( - name: 'man getting massage: light skin tone', - char: '\u{1F486}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_getting_face_massage_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'face', - 'light skin tone', - 'man', - 'massage', - 'uc8', - 'diversity', - 'men', - 'yoga', - 'calm', - 'spa', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna' - ], - modifiable: true), - Emoji( - name: 'man getting massage: medium-light skin tone', - char: '\u{1F486}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_getting_face_massage_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'face', - 'man', - 'massage', - 'medium-light skin tone', - 'uc8', - 'diversity', - 'men', - 'yoga', - 'calm', - 'spa', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna' - ], - modifiable: true), - Emoji( - name: 'man getting massage: medium skin tone', - char: '\u{1F486}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_getting_face_massage_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'face', - 'man', - 'massage', - 'medium skin tone', - 'uc8', - 'diversity', - 'men', - 'yoga', - 'calm', - 'spa', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna' - ], - modifiable: true), - Emoji( - name: 'man getting massage: medium-dark skin tone', - char: '\u{1F486}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_getting_face_massage_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'face', - 'man', - 'massage', - 'medium-dark skin tone', - 'uc8', - 'diversity', - 'men', - 'yoga', - 'calm', - 'spa', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna' - ], - modifiable: true), - Emoji( - name: 'man getting massage: dark skin tone', - char: '\u{1F486}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_getting_face_massage_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dark skin tone', - 'face', - 'man', - 'massage', - 'uc8', - 'diversity', - 'men', - 'yoga', - 'calm', - 'spa', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna' - ], - modifiable: true), - Emoji( - name: 'person in steamy room', - char: '\u{1F9D6}', - shortName: 'person_in_steamy_room', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc10', - 'diversity', - 'hot', - 'steam', - 'girls night', - 'spa', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'heat', - 'warm', - 'caliente', - 'chaud', - 'heiß', - 'steaming', - 'piping', - 'ladies night', - 'girls only', - 'girlfriend', - 'relax', - 'sauna', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'person in steamy room: light skin tone', - char: '\u{1F9D6}\u{1F3FB}', - shortName: 'person_in_steamy_room_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'light skin tone', - 'sauna', - 'steam room', - 'uc10', - 'diversity', - 'hot', - 'steam', - 'girls night', - 'spa', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'heat', - 'warm', - 'caliente', - 'chaud', - 'heiß', - 'steaming', - 'piping', - 'ladies night', - 'girls only', - 'girlfriend', - 'relax', - 'sauna', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person in steamy room: medium-light skin tone', - char: '\u{1F9D6}\u{1F3FC}', - shortName: 'person_in_steamy_room_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'medium-light skin tone', - 'sauna', - 'steam room', - 'uc10', - 'diversity', - 'hot', - 'steam', - 'girls night', - 'spa', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'heat', - 'warm', - 'caliente', - 'chaud', - 'heiß', - 'steaming', - 'piping', - 'ladies night', - 'girls only', - 'girlfriend', - 'relax', - 'sauna', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person in steamy room: medium skin tone', - char: '\u{1F9D6}\u{1F3FD}', - shortName: 'person_in_steamy_room_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'medium skin tone', - 'sauna', - 'steam room', - 'uc10', - 'diversity', - 'hot', - 'steam', - 'girls night', - 'spa', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'heat', - 'warm', - 'caliente', - 'chaud', - 'heiß', - 'steaming', - 'piping', - 'ladies night', - 'girls only', - 'girlfriend', - 'relax', - 'sauna', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person in steamy room: medium-dark skin tone', - char: '\u{1F9D6}\u{1F3FE}', - shortName: 'person_in_steamy_room_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'medium-dark skin tone', - 'sauna', - 'steam room', - 'uc10', - 'diversity', - 'hot', - 'steam', - 'girls night', - 'spa', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'heat', - 'warm', - 'caliente', - 'chaud', - 'heiß', - 'steaming', - 'piping', - 'ladies night', - 'girls only', - 'girlfriend', - 'relax', - 'sauna', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person in steamy room: dark skin tone', - char: '\u{1F9D6}\u{1F3FF}', - shortName: 'person_in_steamy_room_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dark skin tone', - 'sauna', - 'steam room', - 'uc10', - 'diversity', - 'hot', - 'steam', - 'girls night', - 'spa', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'heat', - 'warm', - 'caliente', - 'chaud', - 'heiß', - 'steaming', - 'piping', - 'ladies night', - 'girls only', - 'girlfriend', - 'relax', - 'sauna', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman in steamy room', - char: '\u{1F9D6}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_in_steamy_room', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'sauna', - 'steam room', - 'uc10', - 'diversity', - 'women', - 'hot', - 'steam', - 'girls night', - 'spa', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'heat', - 'warm', - 'caliente', - 'chaud', - 'heiß', - 'steaming', - 'piping', - 'ladies night', - 'girls only', - 'girlfriend', - 'relax', - 'sauna', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'woman in steamy room: light skin tone', - char: '\u{1F9D6}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_in_steamy_room_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'light skin tone', - 'sauna', - 'steam room', - 'uc10', - 'diversity', - 'women', - 'hot', - 'steam', - 'girls night', - 'spa', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'heat', - 'warm', - 'caliente', - 'chaud', - 'heiß', - 'steaming', - 'piping', - 'ladies night', - 'girls only', - 'girlfriend', - 'relax', - 'sauna', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman in steamy room: medium-light skin tone', - char: '\u{1F9D6}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_in_steamy_room_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'medium-light skin tone', - 'sauna', - 'steam room', - 'uc10', - 'diversity', - 'women', - 'hot', - 'steam', - 'girls night', - 'spa', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'heat', - 'warm', - 'caliente', - 'chaud', - 'heiß', - 'steaming', - 'piping', - 'ladies night', - 'girls only', - 'girlfriend', - 'relax', - 'sauna', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman in steamy room: medium skin tone', - char: '\u{1F9D6}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_in_steamy_room_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'medium skin tone', - 'sauna', - 'steam room', - 'uc10', - 'diversity', - 'women', - 'hot', - 'steam', - 'girls night', - 'spa', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'heat', - 'warm', - 'caliente', - 'chaud', - 'heiß', - 'steaming', - 'piping', - 'ladies night', - 'girls only', - 'girlfriend', - 'relax', - 'sauna', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman in steamy room: medium-dark skin tone', - char: '\u{1F9D6}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_in_steamy_room_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'medium-dark skin tone', - 'sauna', - 'steam room', - 'uc10', - 'diversity', - 'women', - 'hot', - 'steam', - 'girls night', - 'spa', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'heat', - 'warm', - 'caliente', - 'chaud', - 'heiß', - 'steaming', - 'piping', - 'ladies night', - 'girls only', - 'girlfriend', - 'relax', - 'sauna', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman in steamy room: dark skin tone', - char: '\u{1F9D6}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_in_steamy_room_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dark skin tone', - 'sauna', - 'steam room', - 'uc10', - 'diversity', - 'women', - 'hot', - 'steam', - 'girls night', - 'spa', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'heat', - 'warm', - 'caliente', - 'chaud', - 'heiß', - 'steaming', - 'piping', - 'ladies night', - 'girls only', - 'girlfriend', - 'relax', - 'sauna', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'man in steamy room', - char: '\u{1F9D6}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_in_steamy_room', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'sauna', - 'steam room', - 'uc10', - 'diversity', - 'hot', - 'steam', - 'spa', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'heat', - 'warm', - 'caliente', - 'chaud', - 'heiß', - 'steaming', - 'piping', - 'relax', - 'sauna' - ]), - Emoji( - name: 'man in steamy room: light skin tone', - char: '\u{1F9D6}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_in_steamy_room_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'light skin tone', - 'sauna', - 'steam room', - 'uc10', - 'diversity', - 'hot', - 'steam', - 'spa', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'heat', - 'warm', - 'caliente', - 'chaud', - 'heiß', - 'steaming', - 'piping', - 'relax', - 'sauna' - ], - modifiable: true), - Emoji( - name: 'man in steamy room: medium-light skin tone', - char: '\u{1F9D6}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_in_steamy_room_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'medium-light skin tone', - 'sauna', - 'steam room', - 'uc10', - 'diversity', - 'hot', - 'steam', - 'spa', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'heat', - 'warm', - 'caliente', - 'chaud', - 'heiß', - 'steaming', - 'piping', - 'relax', - 'sauna' - ], - modifiable: true), - Emoji( - name: 'man in steamy room: medium skin tone', - char: '\u{1F9D6}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_in_steamy_room_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'medium skin tone', - 'sauna', - 'steam room', - 'uc10', - 'diversity', - 'hot', - 'steam', - 'spa', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'heat', - 'warm', - 'caliente', - 'chaud', - 'heiß', - 'steaming', - 'piping', - 'relax', - 'sauna' - ], - modifiable: true), - Emoji( - name: 'man in steamy room: medium-dark skin tone', - char: '\u{1F9D6}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_in_steamy_room_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'medium-dark skin tone', - 'sauna', - 'steam room', - 'uc10', - 'diversity', - 'hot', - 'steam', - 'spa', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'heat', - 'warm', - 'caliente', - 'chaud', - 'heiß', - 'steaming', - 'piping', - 'relax', - 'sauna' - ], - modifiable: true), - Emoji( - name: 'man in steamy room: dark skin tone', - char: '\u{1F9D6}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_in_steamy_room_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dark skin tone', - 'sauna', - 'steam room', - 'uc10', - 'diversity', - 'hot', - 'steam', - 'spa', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'heat', - 'warm', - 'caliente', - 'chaud', - 'heiß', - 'steaming', - 'piping', - 'relax', - 'sauna' - ], - modifiable: true), - Emoji( - name: 'nail polish', - char: '\u{1F485}', - shortName: 'nail_care', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handProp, - keywords: [ - 'care', - 'cosmetics', - 'manicure', - 'nail', - 'polish', - 'uc6', - 'diversity', - 'women', - 'body', - 'hands', - 'nailpolish', - 'beautiful', - 'girls night', - 'painting', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'nails', - 'fingernails', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'painter', - 'arts', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'nail polish: light skin tone', - char: '\u{1F485}\u{1F3FB}', - shortName: 'nail_care_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handProp, - keywords: [ - 'care', - 'cosmetics', - 'light skin tone', - 'manicure', - 'nail', - 'polish', - 'uc8', - 'diversity', - 'women', - 'body', - 'hands', - 'nailpolish', - 'beautiful', - 'girls night', - 'painting', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'nails', - 'fingernails', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'painter', - 'arts', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'nail polish: medium-light skin tone', - char: '\u{1F485}\u{1F3FC}', - shortName: 'nail_care_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handProp, - keywords: [ - 'care', - 'cosmetics', - 'manicure', - 'medium-light skin tone', - 'nail', - 'polish', - 'uc8', - 'diversity', - 'women', - 'body', - 'hands', - 'nailpolish', - 'beautiful', - 'girls night', - 'painting', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'nails', - 'fingernails', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'painter', - 'arts', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'nail polish: medium skin tone', - char: '\u{1F485}\u{1F3FD}', - shortName: 'nail_care_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handProp, - keywords: [ - 'care', - 'cosmetics', - 'manicure', - 'medium skin tone', - 'nail', - 'polish', - 'uc8', - 'diversity', - 'women', - 'body', - 'hands', - 'nailpolish', - 'beautiful', - 'girls night', - 'painting', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'nails', - 'fingernails', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'painter', - 'arts', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'nail polish: medium-dark skin tone', - char: '\u{1F485}\u{1F3FE}', - shortName: 'nail_care_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handProp, - keywords: [ - 'care', - 'cosmetics', - 'manicure', - 'medium-dark skin tone', - 'nail', - 'polish', - 'uc8', - 'diversity', - 'women', - 'body', - 'hands', - 'nailpolish', - 'beautiful', - 'girls night', - 'painting', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'nails', - 'fingernails', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'painter', - 'arts', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'nail polish: dark skin tone', - char: '\u{1F485}\u{1F3FF}', - shortName: 'nail_care_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handProp, - keywords: [ - 'care', - 'cosmetics', - 'dark skin tone', - 'manicure', - 'nail', - 'polish', - 'uc8', - 'diversity', - 'women', - 'body', - 'hands', - 'nailpolish', - 'beautiful', - 'girls night', - 'painting', - 'mom', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'body part', - 'anatomy', - 'hand', - 'finger', - 'fingers', - 'nails', - 'fingernails', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'painter', - 'arts', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'selfie', - char: '\u{1F933}', - shortName: 'selfie', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handProp, - keywords: [ - 'camera', - 'phone', - 'selfie', - 'uc9', - 'diversity', - 'selfie', - 'fame', - 'instagram', - 'fun', - 'youtube', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'famous', - 'celebrity', - 'vlog' - ]), - Emoji( - name: 'selfie: light skin tone', - char: '\u{1F933}\u{1F3FB}', - shortName: 'selfie_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handProp, - keywords: [ - 'camera', - 'light skin tone', - 'phone', - 'selfie', - 'uc9', - 'diversity', - 'selfie', - 'fame', - 'instagram', - 'fun', - 'youtube', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'famous', - 'celebrity', - 'vlog' - ], - modifiable: true), - Emoji( - name: 'selfie: medium-light skin tone', - char: '\u{1F933}\u{1F3FC}', - shortName: 'selfie_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handProp, - keywords: [ - 'camera', - 'medium-light skin tone', - 'phone', - 'selfie', - 'uc9', - 'diversity', - 'selfie', - 'fame', - 'instagram', - 'fun', - 'youtube', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'famous', - 'celebrity', - 'vlog' - ], - modifiable: true), - Emoji( - name: 'selfie: medium skin tone', - char: '\u{1F933}\u{1F3FD}', - shortName: 'selfie_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handProp, - keywords: [ - 'camera', - 'medium skin tone', - 'phone', - 'selfie', - 'uc9', - 'diversity', - 'selfie', - 'fame', - 'instagram', - 'fun', - 'youtube', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'famous', - 'celebrity', - 'vlog' - ], - modifiable: true), - Emoji( - name: 'selfie: medium-dark skin tone', - char: '\u{1F933}\u{1F3FE}', - shortName: 'selfie_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handProp, - keywords: [ - 'camera', - 'medium-dark skin tone', - 'phone', - 'selfie', - 'uc9', - 'diversity', - 'selfie', - 'fame', - 'instagram', - 'fun', - 'youtube', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'famous', - 'celebrity', - 'vlog' - ], - modifiable: true), - Emoji( - name: 'selfie: dark skin tone', - char: '\u{1F933}\u{1F3FF}', - shortName: 'selfie_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.handProp, - keywords: [ - 'camera', - 'dark skin tone', - 'phone', - 'selfie', - 'uc9', - 'diversity', - 'selfie', - 'fame', - 'instagram', - 'fun', - 'youtube', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'famous', - 'celebrity', - 'vlog' - ], - modifiable: true), - Emoji( - name: 'woman dancing', - char: '\u{1F483}', - shortName: 'dancer', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dancing', - 'woman', - 'uc6', - 'instruments', - 'diversity', - 'women', - 'mexican', - 'sexy', - 'circus', - 'beautiful', - 'girls night', - 'dance', - 'hawaii', - 'celebrate', - 'disco', - 'las vegas', - 'fun', - 'activity', - 'dress', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'mexico', - 'cinco de mayo', - 'español', - 'circus tent', - 'clown', - 'clowns', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'dancers', - 'dancing', - 'ballet', - 'ballerina', - 'dabbing', - 'salsa', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'vegas' - ]), - Emoji( - name: 'woman dancing: light skin tone', - char: '\u{1F483}\u{1F3FB}', - shortName: 'dancer_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dancing', - 'light skin tone', - 'woman', - 'uc8', - 'instruments', - 'diversity', - 'women', - 'mexican', - 'sexy', - 'circus', - 'beautiful', - 'girls night', - 'dance', - 'hawaii', - 'celebrate', - 'disco', - 'las vegas', - 'fun', - 'activity', - 'dress', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'mexico', - 'cinco de mayo', - 'español', - 'circus tent', - 'clown', - 'clowns', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'dancers', - 'dancing', - 'ballet', - 'ballerina', - 'dabbing', - 'salsa', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'vegas' - ], - modifiable: true), - Emoji( - name: 'woman dancing: medium-light skin tone', - char: '\u{1F483}\u{1F3FC}', - shortName: 'dancer_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dancing', - 'medium-light skin tone', - 'woman', - 'uc8', - 'instruments', - 'diversity', - 'women', - 'mexican', - 'sexy', - 'circus', - 'beautiful', - 'girls night', - 'dance', - 'hawaii', - 'celebrate', - 'disco', - 'las vegas', - 'fun', - 'activity', - 'dress', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'mexico', - 'cinco de mayo', - 'español', - 'circus tent', - 'clown', - 'clowns', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'dancers', - 'dancing', - 'ballet', - 'ballerina', - 'dabbing', - 'salsa', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'vegas' - ], - modifiable: true), - Emoji( - name: 'woman dancing: medium skin tone', - char: '\u{1F483}\u{1F3FD}', - shortName: 'dancer_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dancing', - 'medium skin tone', - 'woman', - 'uc8', - 'instruments', - 'diversity', - 'women', - 'mexican', - 'sexy', - 'circus', - 'beautiful', - 'girls night', - 'dance', - 'hawaii', - 'celebrate', - 'disco', - 'las vegas', - 'fun', - 'activity', - 'dress', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'mexico', - 'cinco de mayo', - 'español', - 'circus tent', - 'clown', - 'clowns', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'dancers', - 'dancing', - 'ballet', - 'ballerina', - 'dabbing', - 'salsa', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'vegas' - ], - modifiable: true), - Emoji( - name: 'woman dancing: medium-dark skin tone', - char: '\u{1F483}\u{1F3FE}', - shortName: 'dancer_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dancing', - 'medium-dark skin tone', - 'woman', - 'uc8', - 'instruments', - 'diversity', - 'women', - 'mexican', - 'sexy', - 'circus', - 'beautiful', - 'girls night', - 'dance', - 'hawaii', - 'celebrate', - 'disco', - 'las vegas', - 'fun', - 'activity', - 'dress', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'mexico', - 'cinco de mayo', - 'español', - 'circus tent', - 'clown', - 'clowns', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'dancers', - 'dancing', - 'ballet', - 'ballerina', - 'dabbing', - 'salsa', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'vegas' - ], - modifiable: true), - Emoji( - name: 'woman dancing: dark skin tone', - char: '\u{1F483}\u{1F3FF}', - shortName: 'dancer_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dancing', - 'dark skin tone', - 'woman', - 'uc8', - 'instruments', - 'diversity', - 'women', - 'mexican', - 'sexy', - 'circus', - 'beautiful', - 'girls night', - 'dance', - 'hawaii', - 'celebrate', - 'disco', - 'las vegas', - 'fun', - 'activity', - 'dress', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'mexico', - 'cinco de mayo', - 'español', - 'circus tent', - 'clown', - 'clowns', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'dancers', - 'dancing', - 'ballet', - 'ballerina', - 'dabbing', - 'salsa', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'vegas' - ], - modifiable: true), - Emoji( - name: 'man dancing', - char: '\u{1F57A}', - shortName: 'man_dancing', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dance', - 'man', - 'uc9', - 'instruments', - 'diversity', - 'men', - 'boys night', - 'dance', - 'celebrate', - 'disco', - 'daddy', - 'las vegas', - 'fun', - 'activity', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'dancers', - 'dancing', - 'ballet', - 'ballerina', - 'dabbing', - 'salsa', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'dad', - 'papa', - 'pere', - 'father', - 'vegas' - ]), - Emoji( - name: 'man dancing: light skin tone', - char: '\u{1F57A}\u{1F3FB}', - shortName: 'man_dancing_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dance', - 'light skin tone', - 'man', - 'uc9', - 'instruments', - 'diversity', - 'men', - 'boys night', - 'dance', - 'celebrate', - 'disco', - 'daddy', - 'las vegas', - 'fun', - 'activity', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'dancers', - 'dancing', - 'ballet', - 'ballerina', - 'dabbing', - 'salsa', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'dad', - 'papa', - 'pere', - 'father', - 'vegas' - ], - modifiable: true), - Emoji( - name: 'man dancing: medium-light skin tone', - char: '\u{1F57A}\u{1F3FC}', - shortName: 'man_dancing_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dance', - 'man', - 'medium-light skin tone', - 'uc9', - 'instruments', - 'diversity', - 'men', - 'boys night', - 'dance', - 'celebrate', - 'disco', - 'daddy', - 'las vegas', - 'fun', - 'activity', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'dancers', - 'dancing', - 'ballet', - 'ballerina', - 'dabbing', - 'salsa', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'dad', - 'papa', - 'pere', - 'father', - 'vegas' - ], - modifiable: true), - Emoji( - name: 'man dancing: medium skin tone', - char: '\u{1F57A}\u{1F3FD}', - shortName: 'man_dancing_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dance', - 'man', - 'medium skin tone', - 'uc9', - 'instruments', - 'diversity', - 'men', - 'boys night', - 'dance', - 'celebrate', - 'disco', - 'daddy', - 'las vegas', - 'fun', - 'activity', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'dancers', - 'dancing', - 'ballet', - 'ballerina', - 'dabbing', - 'salsa', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'dad', - 'papa', - 'pere', - 'father', - 'vegas' - ], - modifiable: true), - Emoji( - name: 'man dancing: dark skin tone', - char: '\u{1F57A}\u{1F3FF}', - shortName: 'man_dancing_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dance', - 'dark skin tone', - 'man', - 'uc9', - 'instruments', - 'diversity', - 'men', - 'boys night', - 'dance', - 'celebrate', - 'disco', - 'daddy', - 'las vegas', - 'fun', - 'activity', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'dancers', - 'dancing', - 'ballet', - 'ballerina', - 'dabbing', - 'salsa', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'dad', - 'papa', - 'pere', - 'father', - 'vegas' - ], - modifiable: true), - Emoji( - name: 'man dancing: medium-dark skin tone', - char: '\u{1F57A}\u{1F3FE}', - shortName: 'man_dancing_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dance', - 'man', - 'medium-dark skin tone', - 'uc9', - 'instruments', - 'diversity', - 'men', - 'boys night', - 'dance', - 'celebrate', - 'disco', - 'daddy', - 'las vegas', - 'fun', - 'activity', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'dancers', - 'dancing', - 'ballet', - 'ballerina', - 'dabbing', - 'salsa', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'dad', - 'papa', - 'pere', - 'father', - 'vegas' - ], - modifiable: true), - Emoji( - name: 'people with bunny ears', - char: '\u{1F46F}', - shortName: 'people_with_bunny_ears_partying', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'bunny ear', - 'dancer', - 'partying', - 'uc6', - 'instruments', - 'silly', - 'halloween', - 'men', - 'japan', - 'sexy', - 'girls night', - 'boys night', - 'dance', - 'easter', - 'porn', - 'las vegas', - 'fun', - 'crazy', - 'playboy', - 'activity', - 'disguise', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'funny', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'japanese', - 'ninja', - 'ladies night', - 'girls only', - 'girlfriend', - 'guys night', - 'dancers', - 'dancing', - 'ballet', - 'ballerina', - 'dabbing', - 'salsa', - 'vegas', - 'weird', - 'awkward', - 'insane', - 'wild', - 'play boy' - ]), - Emoji( - name: 'women with bunny ears', - char: '\u{1F46F}\u{200D}\u{2640}\u{FE0F}', - shortName: 'women_with_bunny_ears_partying', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'bunny ear', - 'dancer', - 'partying', - 'women', - 'uc6', - 'instruments', - 'silly', - 'women', - 'halloween', - 'japan', - 'girls night', - 'boys night', - 'dance', - 'easter', - 'las vegas', - 'fun', - 'crazy', - 'playboy', - 'activity', - 'disguise', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'funny', - 'woman', - 'female', - 'samhain', - 'japanese', - 'ninja', - 'ladies night', - 'girls only', - 'girlfriend', - 'guys night', - 'dancers', - 'dancing', - 'ballet', - 'ballerina', - 'dabbing', - 'salsa', - 'vegas', - 'weird', - 'awkward', - 'insane', - 'wild', - 'play boy' - ]), - Emoji( - name: 'men with bunny ears', - char: '\u{1F46F}\u{200D}\u{2642}\u{FE0F}', - shortName: 'men_with_bunny_ears_partying', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'bunny ear', - 'dancer', - 'men', - 'partying', - 'uc6', - 'instruments', - 'silly', - 'halloween', - 'men', - 'japan', - 'girls night', - 'boys night', - 'dance', - 'queen', - 'easter', - 'las vegas', - 'fun', - 'crazy', - 'playboy', - 'activity', - 'disguise', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'funny', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'japanese', - 'ninja', - 'ladies night', - 'girls only', - 'girlfriend', - 'guys night', - 'dancers', - 'dancing', - 'ballet', - 'ballerina', - 'dabbing', - 'salsa', - 'king', - 'prince', - 'princess', - 'vegas', - 'weird', - 'awkward', - 'insane', - 'wild', - 'play boy' - ]), - Emoji( - name: 'person in suit levitating', - char: '\u{1F574}\u{FE0F}', - shortName: 'levitate', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'business', - 'man', - 'suit', - 'uc7', - 'halloween', - 'men', - 'job', - 'business', - 'sunglasses', - 'google', - 'detective', - 'fame', - 'gangster', - 'super hero', - 'vampire', - 'las vegas', - 'mystery', - 'disguise', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'profession', - 'boss', - 'career', - 'shades', - 'lunettes de soleil', - 'sun glasses', - 'famous', - 'celebrity', - 'thug', - 'superhero', - 'superman', - 'batman', - 'dracula', - 'vegas' - ]), - Emoji( - name: 'person in suit levitating: light skin tone', - char: '\u{1F574}\u{1F3FB}', - shortName: 'levitate_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'business', - 'light skin tone', - 'man', - 'suit', - 'uc8', - 'halloween', - 'men', - 'job', - 'business', - 'sunglasses', - 'google', - 'detective', - 'fame', - 'gangster', - 'super hero', - 'vampire', - 'las vegas', - 'mystery', - 'disguise', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'profession', - 'boss', - 'career', - 'shades', - 'lunettes de soleil', - 'sun glasses', - 'famous', - 'celebrity', - 'thug', - 'superhero', - 'superman', - 'batman', - 'dracula', - 'vegas' - ], - modifiable: true), - Emoji( - name: 'person in suit levitating: medium-light skin tone', - char: '\u{1F574}\u{1F3FC}', - shortName: 'levitate_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'business', - 'man', - 'medium-light skin tone', - 'suit', - 'uc8', - 'halloween', - 'men', - 'job', - 'business', - 'sunglasses', - 'google', - 'detective', - 'fame', - 'gangster', - 'super hero', - 'vampire', - 'las vegas', - 'mystery', - 'disguise', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'profession', - 'boss', - 'career', - 'shades', - 'lunettes de soleil', - 'sun glasses', - 'famous', - 'celebrity', - 'thug', - 'superhero', - 'superman', - 'batman', - 'dracula', - 'vegas' - ], - modifiable: true), - Emoji( - name: 'person in suit levitating: medium skin tone', - char: '\u{1F574}\u{1F3FD}', - shortName: 'levitate_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'business', - 'man', - 'medium skin tone', - 'suit', - 'uc8', - 'halloween', - 'men', - 'job', - 'business', - 'sunglasses', - 'google', - 'detective', - 'fame', - 'gangster', - 'super hero', - 'vampire', - 'las vegas', - 'mystery', - 'disguise', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'profession', - 'boss', - 'career', - 'shades', - 'lunettes de soleil', - 'sun glasses', - 'famous', - 'celebrity', - 'thug', - 'superhero', - 'superman', - 'batman', - 'dracula', - 'vegas' - ], - modifiable: true), - Emoji( - name: 'person in suit levitating: medium-dark skin tone', - char: '\u{1F574}\u{1F3FE}', - shortName: 'levitate_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'business', - 'man', - 'medium-dark skin tone', - 'suit', - 'uc8', - 'halloween', - 'men', - 'job', - 'business', - 'sunglasses', - 'google', - 'detective', - 'fame', - 'gangster', - 'super hero', - 'vampire', - 'las vegas', - 'mystery', - 'disguise', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'profession', - 'boss', - 'career', - 'shades', - 'lunettes de soleil', - 'sun glasses', - 'famous', - 'celebrity', - 'thug', - 'superhero', - 'superman', - 'batman', - 'dracula', - 'vegas' - ], - modifiable: true), - Emoji( - name: 'person in suit levitating: dark skin tone', - char: '\u{1F574}\u{1F3FF}', - shortName: 'levitate_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'business', - 'dark skin tone', - 'man', - 'suit', - 'uc8', - 'halloween', - 'men', - 'job', - 'business', - 'sunglasses', - 'google', - 'detective', - 'fame', - 'gangster', - 'super hero', - 'vampire', - 'las vegas', - 'mystery', - 'disguise', - 'samhain', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'profession', - 'boss', - 'career', - 'shades', - 'lunettes de soleil', - 'sun glasses', - 'famous', - 'celebrity', - 'thug', - 'superhero', - 'superman', - 'batman', - 'dracula', - 'vegas' - ], - modifiable: true), - Emoji( - name: 'person in manual wheelchair', - char: '\u{1F9D1}\u{200D}\u{1F9BD}', - shortName: 'person_in_manual_wheelchair', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'disabled', - 'disability' - ]), - Emoji( - name: 'person in manual wheelchair: light skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F9BD}', - shortName: 'person_in_manual_wheelchair_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'person in manual wheelchair: medium-light skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F9BD}', - shortName: 'person_in_manual_wheelchair_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'person in manual wheelchair: medium skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F9BD}', - shortName: 'person_in_manual_wheelchair_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'person in manual wheelchair: medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F9BD}', - shortName: 'person_in_manual_wheelchair_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'person in manual wheelchair: dark skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F9BD}', - shortName: 'person_in_manual_wheelchair_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'woman in manual wheelchair', - char: '\u{1F469}\u{200D}\u{1F9BD}', - shortName: 'woman_in_manual_wheelchair', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability' - ]), - Emoji( - name: 'woman in manual wheelchair: light skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F9BD}', - shortName: 'woman_in_manual_wheelchair_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'woman in manual wheelchair: medium-light skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F9BD}', - shortName: 'woman_in_manual_wheelchair_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'woman in manual wheelchair: medium skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F9BD}', - shortName: 'woman_in_manual_wheelchair_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'woman in manual wheelchair: medium-dark skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F9BD}', - shortName: 'woman_in_manual_wheelchair_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'woman in manual wheelchair: dark skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F9BD}', - shortName: 'woman_in_manual_wheelchair_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'man in manual wheelchair', - char: '\u{1F468}\u{200D}\u{1F9BD}', - shortName: 'man_in_manual_wheelchair', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability' - ]), - Emoji( - name: 'man in manual wheelchair: light skin tone', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{1F9BD}', - shortName: 'man_in_manual_wheelchair_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'man in manual wheelchair: medium-light skin tone', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{1F9BD}', - shortName: 'man_in_manual_wheelchair_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'man in manual wheelchair: medium skin tone', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{1F9BD}', - shortName: 'man_in_manual_wheelchair_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'man in manual wheelchair: medium-dark skin tone', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{1F9BD}', - shortName: 'man_in_manual_wheelchair_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'man in manual wheelchair: dark skin tone', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{1F9BD}', - shortName: 'man_in_manual_wheelchair_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'person in motorized wheelchair', - char: '\u{1F9D1}\u{200D}\u{1F9BC}', - shortName: 'person_in_motorized_wheelchair', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'disabled', - 'disability' - ]), - Emoji( - name: 'person in motorized wheelchair: light skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F9BC}', - shortName: 'person_in_motorized_wheelchair_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'person in motorized wheelchair: medium-light skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F9BC}', - shortName: 'person_in_motorized_wheelchair_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'person in motorized wheelchair: medium skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F9BC}', - shortName: 'person_in_motorized_wheelchair_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'person in motorized wheelchair: medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F9BC}', - shortName: 'person_in_motorized_wheelchair_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'person in motorized wheelchair: dark skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F9BC}', - shortName: 'person_in_motorized_wheelchair_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'woman in motorized wheelchair', - char: '\u{1F469}\u{200D}\u{1F9BC}', - shortName: 'woman_in_motorized_wheelchair', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability' - ]), - Emoji( - name: 'woman in motorized wheelchair: light skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F9BC}', - shortName: 'woman_in_motorized_wheelchair_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'woman in motorized wheelchair: medium-light skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F9BC}', - shortName: 'woman_in_motorized_wheelchair_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'woman in motorized wheelchair: medium skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F9BC}', - shortName: 'woman_in_motorized_wheelchair_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'woman in motorized wheelchair: medium-dark skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F9BC}', - shortName: 'woman_in_motorized_wheelchair_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'woman in motorized wheelchair: dark skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F9BC}', - shortName: 'woman_in_motorized_wheelchair_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'man in motorized wheelchair', - char: '\u{1F468}\u{200D}\u{1F9BC}', - shortName: 'man_in_motorized_wheelchair', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability' - ]), - Emoji( - name: 'man in motorized wheelchair: light skin tone', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{1F9BC}', - shortName: 'man_in_motorized_wheelchair_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'man in motorized wheelchair: medium-light skin tone', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{1F9BC}', - shortName: 'man_in_motorized_wheelchair_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'man in motorized wheelchair: medium skin tone', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{1F9BC}', - shortName: 'man_in_motorized_wheelchair_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'man in motorized wheelchair: medium-dark skin tone', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{1F9BC}', - shortName: 'man_in_motorized_wheelchair_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'man in motorized wheelchair: dark skin tone', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{1F9BC}', - shortName: 'man_in_motorized_wheelchair_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'old people', - 'diversity', - 'handicap', - 'accessibility', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability' - ], - modifiable: true), - Emoji( - name: 'person walking', - char: '\u{1F6B6}', - shortName: 'person_walking', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'hike', - 'walk', - 'walking', - 'uc6', - 'sport', - 'diversity', - 'men', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male' - ]), - Emoji( - name: 'person walking: light skin tone', - char: '\u{1F6B6}\u{1F3FB}', - shortName: 'person_walking_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'hike', - 'light skin tone', - 'walk', - 'walking', - 'uc8', - 'sport', - 'diversity', - 'men', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male' - ], - modifiable: true), - Emoji( - name: 'person walking: medium-light skin tone', - char: '\u{1F6B6}\u{1F3FC}', - shortName: 'person_walking_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'hike', - 'medium-light skin tone', - 'walk', - 'walking', - 'uc8', - 'sport', - 'diversity', - 'men', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male' - ], - modifiable: true), - Emoji( - name: 'person walking: medium skin tone', - char: '\u{1F6B6}\u{1F3FD}', - shortName: 'person_walking_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'hike', - 'medium skin tone', - 'walk', - 'walking', - 'uc8', - 'sport', - 'diversity', - 'men', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male' - ], - modifiable: true), - Emoji( - name: 'person walking: medium-dark skin tone', - char: '\u{1F6B6}\u{1F3FE}', - shortName: 'person_walking_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'hike', - 'medium-dark skin tone', - 'walk', - 'walking', - 'uc8', - 'sport', - 'diversity', - 'men', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male' - ], - modifiable: true), - Emoji( - name: 'person walking: dark skin tone', - char: '\u{1F6B6}\u{1F3FF}', - shortName: 'person_walking_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dark skin tone', - 'hike', - 'walk', - 'walking', - 'uc8', - 'sport', - 'diversity', - 'men', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male' - ], - modifiable: true), - Emoji( - name: 'woman walking', - char: '\u{1F6B6}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_walking', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'hike', - 'walk', - 'woman', - 'uc6', - 'sport', - 'diversity', - 'women', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female' - ]), - Emoji( - name: 'woman walking: light skin tone', - char: '\u{1F6B6}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_walking_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'hike', - 'light skin tone', - 'walk', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'women', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female' - ], - modifiable: true), - Emoji( - name: 'woman walking: medium-light skin tone', - char: '\u{1F6B6}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_walking_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'hike', - 'medium-light skin tone', - 'walk', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'women', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female' - ], - modifiable: true), - Emoji( - name: 'woman walking: medium skin tone', - char: '\u{1F6B6}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_walking_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'hike', - 'medium skin tone', - 'walk', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'women', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female' - ], - modifiable: true), - Emoji( - name: 'woman walking: medium-dark skin tone', - char: '\u{1F6B6}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_walking_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'hike', - 'medium-dark skin tone', - 'walk', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'women', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female' - ], - modifiable: true), - Emoji( - name: 'woman walking: dark skin tone', - char: '\u{1F6B6}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_walking_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dark skin tone', - 'hike', - 'walk', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'women', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female' - ], - modifiable: true), - Emoji( - name: 'man walking', - char: '\u{1F6B6}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_walking', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'hike', - 'man', - 'walk', - 'uc6', - 'sport', - 'diversity', - 'men', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male' - ]), - Emoji( - name: 'man walking: light skin tone', - char: '\u{1F6B6}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_walking_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'hike', - 'light skin tone', - 'man', - 'walk', - 'uc8', - 'sport', - 'diversity', - 'men', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male' - ], - modifiable: true), - Emoji( - name: 'man walking: medium-light skin tone', - char: '\u{1F6B6}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_walking_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'hike', - 'man', - 'medium-light skin tone', - 'walk', - 'uc8', - 'sport', - 'diversity', - 'men', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male' - ], - modifiable: true), - Emoji( - name: 'man walking: medium skin tone', - char: '\u{1F6B6}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_walking_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'hike', - 'man', - 'medium skin tone', - 'walk', - 'uc8', - 'sport', - 'diversity', - 'men', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male' - ], - modifiable: true), - Emoji( - name: 'man walking: medium-dark skin tone', - char: '\u{1F6B6}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_walking_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'hike', - 'man', - 'medium-dark skin tone', - 'walk', - 'uc8', - 'sport', - 'diversity', - 'men', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male' - ], - modifiable: true), - Emoji( - name: 'man walking: dark skin tone', - char: '\u{1F6B6}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_walking_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dark skin tone', - 'hike', - 'man', - 'walk', - 'uc8', - 'sport', - 'diversity', - 'men', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male' - ], - modifiable: true), - Emoji( - name: 'person with white cane: light skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F9AF}', - shortName: 'person_with_probing_cane_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'accessibility', - 'cane', - 'handicap', - 'blind', - 'probe', - 'disabled', - 'disability', - 'white cane' - ], - modifiable: true), - Emoji( - name: 'person with white cane', - char: '\u{1F9D1}\u{200D}\u{1F9AF}', - shortName: 'person_with_probing_cane', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'cane', - 'handicap', - 'blind', - 'probe', - 'disabled', - 'disability', - 'white cane' - ]), - Emoji( - name: 'person with white cane: medium-light skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F9AF}', - shortName: 'person_with_probing_cane_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'cane', - 'handicap', - 'blind', - 'probe', - 'disabled', - 'disability', - 'white cane' - ], - modifiable: true), - Emoji( - name: 'person with white cane: medium skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F9AF}', - shortName: 'person_with_probing_cane_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'cane', - 'handicap', - 'blind', - 'probe', - 'disabled', - 'disability', - 'white cane' - ], - modifiable: true), - Emoji( - name: 'person with white cane: medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F9AF}', - shortName: 'person_with_probing_cane_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'cane', - 'handicap', - 'blind', - 'probe', - 'disabled', - 'disability', - 'white cane' - ], - modifiable: true), - Emoji( - name: 'person with white cane: dark skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F9AF}', - shortName: 'person_with_probing_cane_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'cane', - 'handicap', - 'blind', - 'probe', - 'disabled', - 'disability', - 'white cane' - ], - modifiable: true), - Emoji( - name: 'woman with white cane', - char: '\u{1F469}\u{200D}\u{1F9AF}', - shortName: 'woman_with_probing_cane', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'handicap', - 'navigate', - 'blind', - 'probe', - 'accessibility', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability', - 'white cane' - ]), - Emoji( - name: 'woman with white cane: light skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F9AF}', - shortName: 'woman_with_probing_cane_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'cane', - 'diversity', - 'handicap', - 'navigate', - 'blind', - 'probe', - 'accessibility', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability', - 'white cane' - ], - modifiable: true), - Emoji( - name: 'woman with white cane: medium-light skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F9AF}', - shortName: 'woman_with_probing_cane_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'handicap', - 'navigate', - 'blind', - 'probe', - 'accessibility', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability', - 'white cane' - ], - modifiable: true), - Emoji( - name: 'woman with white cane: medium skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F9AF}', - shortName: 'woman_with_probing_cane_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'handicap', - 'navigate', - 'blind', - 'probe', - 'accessibility', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability', - 'white cane' - ], - modifiable: true), - Emoji( - name: 'woman with white cane: medium-dark skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F9AF}', - shortName: 'woman_with_probing_cane_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'handicap', - 'navigate', - 'blind', - 'probe', - 'accessibility', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability', - 'white cane' - ], - modifiable: true), - Emoji( - name: 'woman with white cane: dark skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F9AF}', - shortName: 'woman_with_probing_cane_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'handicap', - 'navigate', - 'blind', - 'probe', - 'accessibility', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability', - 'white cane' - ], - modifiable: true), - Emoji( - name: 'man with white cane', - char: '\u{1F468}\u{200D}\u{1F9AF}', - shortName: 'man_with_probing_cane', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'cane', - 'handicap', - 'navigate', - 'blind', - 'probe', - 'accessibility', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability', - 'white cane' - ]), - Emoji( - name: 'man with white cane: light skin tone', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{1F9AF}', - shortName: 'man_with_probing_cane_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'cane', - 'handicap', - 'navigate', - 'blind', - 'probe', - 'accessibility', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability', - 'white cane' - ], - modifiable: true), - Emoji( - name: 'man with white cane: medium skin tone', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{1F9AF}', - shortName: 'man_with_probing_cane_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'cane', - 'handicap', - 'navigate', - 'blind', - 'probe', - 'accessibility', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability', - 'white cane' - ], - modifiable: true), - Emoji( - name: 'man with white cane: medium-light skin tone', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{1F9AF}', - shortName: 'man_with_probing_cane_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'cane', - 'handicap', - 'navigate', - 'blind', - 'probe', - 'accessibility', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability', - 'white cane' - ], - modifiable: true), - Emoji( - name: 'man with white cane: medium-dark skin tone', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{1F9AF}', - shortName: 'man_with_probing_cane_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'cane', - 'handicap', - 'navigate', - 'blind', - 'probe', - 'accessibility', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability', - 'white cane' - ], - modifiable: true), - Emoji( - name: 'man with white cane: dark skin tone', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{1F9AF}', - shortName: 'man_with_probing_cane_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'cane', - 'handicap', - 'navigate', - 'blind', - 'probe', - 'accessibility', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'disabled', - 'disability', - 'white cane' - ], - modifiable: true), - Emoji( - name: 'person kneeling', - char: '\u{1F9CE}', - shortName: 'person_kneeling', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'sit', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'sitting', - 'kneel', - 'kneeling' - ]), - Emoji( - name: 'person kneeling: light skin tone', - char: '\u{1F9CE}\u{1F3FB}', - shortName: 'person_kneeling_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'sit', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'person kneeling: medium-light skin tone', - char: '\u{1F9CE}\u{1F3FC}', - shortName: 'person_kneeling_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'sit', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'person kneeling: medium skin tone', - char: '\u{1F9CE}\u{1F3FD}', - shortName: 'person_kneeling_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'sit', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'person kneeling: medium-dark skin tone', - char: '\u{1F9CE}\u{1F3FE}', - shortName: 'person_kneeling_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'sit', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'person kneeling: dark skin tone', - char: '\u{1F9CE}\u{1F3FF}', - shortName: 'person_kneeling_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'sit', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'woman kneeling', - char: '\u{1F9CE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_kneeling', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'sit', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'sitting', - 'kneel', - 'kneeling' - ]), - Emoji( - name: 'woman kneeling: light skin tone', - char: '\u{1F9CE}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_kneeling_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'sit', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'woman kneeling: medium-light skin tone', - char: '\u{1F9CE}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_kneeling_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'sit', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'woman kneeling: medium skin tone', - char: '\u{1F9CE}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_kneeling_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'sit', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'woman kneeling: medium-dark skin tone', - char: '\u{1F9CE}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_kneeling_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'sit', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'woman kneeling: dark skin tone', - char: '\u{1F9CE}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_kneeling_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'sit', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'man kneeling', - char: '\u{1F9CE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_kneeling', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'sit', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'sitting', - 'kneel', - 'kneeling' - ]), - Emoji( - name: 'man kneeling: light skin tone', - char: '\u{1F9CE}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_kneeling_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'sit', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'man kneeling: medium-light skin tone', - char: '\u{1F9CE}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_kneeling_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'sit', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'man kneeling: medium skin tone', - char: '\u{1F9CE}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_kneeling_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'sit', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'man kneeling: medium-dark skin tone', - char: '\u{1F9CE}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_kneeling_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'sit', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'man kneeling: dark skin tone', - char: '\u{1F9CE}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_kneeling_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'sit', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'person running', - char: '\u{1F3C3}', - shortName: 'person_running', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'marathon', - 'running', - 'uc6', - 'sport', - 'diversity', - 'men', - 'boys night', - 'run', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'running', - 'jog', - 'runner' - ]), - Emoji( - name: 'person running: light skin tone', - char: '\u{1F3C3}\u{1F3FB}', - shortName: 'person_running_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'light skin tone', - 'marathon', - 'running', - 'uc8', - 'sport', - 'diversity', - 'men', - 'boys night', - 'run', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'running', - 'jog', - 'runner' - ], - modifiable: true), - Emoji( - name: 'person running: medium-light skin tone', - char: '\u{1F3C3}\u{1F3FC}', - shortName: 'person_running_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'marathon', - 'medium-light skin tone', - 'running', - 'uc8', - 'sport', - 'diversity', - 'men', - 'boys night', - 'run', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'running', - 'jog', - 'runner' - ], - modifiable: true), - Emoji( - name: 'person running: medium skin tone', - char: '\u{1F3C3}\u{1F3FD}', - shortName: 'person_running_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'marathon', - 'medium skin tone', - 'running', - 'uc8', - 'sport', - 'diversity', - 'men', - 'boys night', - 'run', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'running', - 'jog', - 'runner' - ], - modifiable: true), - Emoji( - name: 'person running: medium-dark skin tone', - char: '\u{1F3C3}\u{1F3FE}', - shortName: 'person_running_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'marathon', - 'medium-dark skin tone', - 'running', - 'uc8', - 'sport', - 'diversity', - 'men', - 'boys night', - 'run', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'running', - 'jog', - 'runner' - ], - modifiable: true), - Emoji( - name: 'person running: dark skin tone', - char: '\u{1F3C3}\u{1F3FF}', - shortName: 'person_running_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dark skin tone', - 'marathon', - 'running', - 'uc8', - 'sport', - 'diversity', - 'men', - 'boys night', - 'run', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'running', - 'jog', - 'runner' - ], - modifiable: true), - Emoji( - name: 'woman running', - char: '\u{1F3C3}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_running', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'marathon', - 'racing', - 'running', - 'woman', - 'uc6', - 'sport', - 'diversity', - 'women', - 'run', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'running', - 'jog', - 'runner' - ]), - Emoji( - name: 'woman running: light skin tone', - char: '\u{1F3C3}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_running_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'light skin tone', - 'marathon', - 'racing', - 'running', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'women', - 'run', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'running', - 'jog', - 'runner' - ], - modifiable: true), - Emoji( - name: 'woman running: medium-light skin tone', - char: '\u{1F3C3}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_running_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'marathon', - 'medium-light skin tone', - 'racing', - 'running', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'women', - 'run', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'running', - 'jog', - 'runner' - ], - modifiable: true), - Emoji( - name: 'woman running: medium skin tone', - char: '\u{1F3C3}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_running_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'marathon', - 'medium skin tone', - 'racing', - 'running', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'women', - 'run', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'running', - 'jog', - 'runner' - ], - modifiable: true), - Emoji( - name: 'woman running: medium-dark skin tone', - char: '\u{1F3C3}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_running_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'marathon', - 'medium-dark skin tone', - 'racing', - 'running', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'women', - 'run', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'running', - 'jog', - 'runner' - ], - modifiable: true), - Emoji( - name: 'woman running: dark skin tone', - char: '\u{1F3C3}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_running_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dark skin tone', - 'marathon', - 'racing', - 'running', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'women', - 'run', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'running', - 'jog', - 'runner' - ], - modifiable: true), - Emoji( - name: 'man running', - char: '\u{1F3C3}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_running', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'man', - 'marathon', - 'racing', - 'running', - 'uc6', - 'sport', - 'men', - 'boys night', - 'run', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'running', - 'jog', - 'runner' - ]), - Emoji( - name: 'man running: light skin tone', - char: '\u{1F3C3}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_running_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'light skin tone', - 'man', - 'marathon', - 'racing', - 'running', - 'uc8', - 'sport', - 'men', - 'boys night', - 'run', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'running', - 'jog', - 'runner' - ], - modifiable: true), - Emoji( - name: 'man running: medium-light skin tone', - char: '\u{1F3C3}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_running_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'man', - 'marathon', - 'medium-light skin tone', - 'racing', - 'running', - 'uc8', - 'sport', - 'men', - 'boys night', - 'run', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'running', - 'jog', - 'runner' - ], - modifiable: true), - Emoji( - name: 'man running: medium skin tone', - char: '\u{1F3C3}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_running_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'man', - 'marathon', - 'medium skin tone', - 'racing', - 'running', - 'uc8', - 'sport', - 'men', - 'boys night', - 'run', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'running', - 'jog', - 'runner' - ], - modifiable: true), - Emoji( - name: 'man running: medium-dark skin tone', - char: '\u{1F3C3}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_running_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'man', - 'marathon', - 'medium-dark skin tone', - 'racing', - 'running', - 'uc8', - 'sport', - 'men', - 'boys night', - 'run', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'running', - 'jog', - 'runner' - ], - modifiable: true), - Emoji( - name: 'man running: dark skin tone', - char: '\u{1F3C3}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_running_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'dark skin tone', - 'man', - 'marathon', - 'racing', - 'running', - 'uc8', - 'sport', - 'men', - 'boys night', - 'run', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'guys night', - 'running', - 'jog', - 'runner' - ], - modifiable: true), - Emoji( - name: 'person standing', - char: '\u{1F9CD}', - shortName: 'person_standing', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'wife', - 'husband', - 'mom', - 'stand', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'person standing: light skin tone', - char: '\u{1F9CD}\u{1F3FB}', - shortName: 'person_standing_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'wife', - 'husband', - 'mom', - 'stand', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person standing: medium-light skin tone', - char: '\u{1F9CD}\u{1F3FC}', - shortName: 'person_standing_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'wife', - 'husband', - 'mom', - 'stand', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person standing: medium skin tone', - char: '\u{1F9CD}\u{1F3FD}', - shortName: 'person_standing_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'wife', - 'husband', - 'mom', - 'stand', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person standing: medium-dark skin tone', - char: '\u{1F9CD}\u{1F3FE}', - shortName: 'person_standing_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'wife', - 'husband', - 'mom', - 'stand', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'person standing: dark skin tone', - char: '\u{1F9CD}\u{1F3FF}', - shortName: 'person_standing_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'wife', - 'husband', - 'mom', - 'stand', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman standing', - char: '\u{1F9CD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_standing', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'human', - 'parent', - 'wife', - 'mom', - 'stand', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'woman standing: light skin tone', - char: '\u{1F9CD}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_standing_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'human', - 'parent', - 'wife', - 'mom', - 'stand', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman standing: medium-light skin tone', - char: '\u{1F9CD}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_standing_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'human', - 'parent', - 'wife', - 'mom', - 'stand', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman standing: medium skin tone', - char: '\u{1F9CD}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_standing_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'human', - 'parent', - 'wife', - 'mom', - 'stand', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman standing: medium-dark skin tone', - char: '\u{1F9CD}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_standing_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'human', - 'parent', - 'wife', - 'mom', - 'stand', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'woman standing: dark skin tone', - char: '\u{1F9CD}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_standing_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'human', - 'parent', - 'wife', - 'mom', - 'stand', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'man standing', - char: '\u{1F9CD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_standing', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'husband', - 'stand', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ]), - Emoji( - name: 'man standing: light skin tone', - char: '\u{1F9CD}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_standing_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'husband', - 'stand', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'man standing: medium-light skin tone', - char: '\u{1F9CD}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_standing_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'husband', - 'stand', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'man standing: medium skin tone', - char: '\u{1F9CD}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_standing_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'husband', - 'stand', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'man standing: medium-dark skin tone', - char: '\u{1F9CD}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_standing_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'husband', - 'stand', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'man standing: dark skin tone', - char: '\u{1F9CD}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_standing_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'uc12', - 'diversity', - 'men', - 'human', - 'daddy', - 'parent', - 'husband', - 'stand', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'people holding hands', - char: '\u{1F9D1}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}', - shortName: 'people_holding_hands', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ]), - Emoji( - name: 'people holding hands: light skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}\u{1F3FB}', - shortName: 'people_holding_hands_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'people holding hands: light skin tone, medium-light skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}\u{1F3FC}', - shortName: 'people_holding_hands_tone1_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'people holding hands: light skin tone, medium skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}\u{1F3FD}', - shortName: 'people_holding_hands_tone1_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'people holding hands: light skin tone, medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}\u{1F3FE}', - shortName: 'people_holding_hands_tone1_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'people holding hands: light skin tone, dark skin tone', - char: '\u{1F9D1}\u{1F3FB}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}\u{1F3FF}', - shortName: 'people_holding_hands_tone1_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'people holding hands: medium-light skin tone, light skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}\u{1F3FB}', - shortName: 'people_holding_hands_tone2_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'people holding hands: medium-light skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}\u{1F3FC}', - shortName: 'people_holding_hands_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'people holding hands: medium-light skin tone, medium skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}\u{1F3FD}', - shortName: 'people_holding_hands_tone2_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: - 'people holding hands: medium-light skin tone, medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}\u{1F3FE}', - shortName: 'people_holding_hands_tone2_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'people holding hands: medium-light skin tone, dark skin tone', - char: '\u{1F9D1}\u{1F3FC}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}\u{1F3FF}', - shortName: 'people_holding_hands_tone2_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'people holding hands: medium skin tone, light skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}\u{1F3FB}', - shortName: 'people_holding_hands_tone3_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'people holding hands: medium skin tone, medium-light skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}\u{1F3FC}', - shortName: 'people_holding_hands_tone3_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'people holding hands: medium skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}\u{1F3FD}', - shortName: 'people_holding_hands_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'people holding hands: medium skin tone, medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}\u{1F3FE}', - shortName: 'people_holding_hands_tone3_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'people holding hands: medium skin tone, dark skin tone', - char: '\u{1F9D1}\u{1F3FD}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}\u{1F3FF}', - shortName: 'people_holding_hands_tone3_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'people holding hands: medium-dark skin tone, light skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}\u{1F3FB}', - shortName: 'people_holding_hands_tone4_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: - 'people holding hands: medium-dark skin tone, medium-light skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}\u{1F3FC}', - shortName: 'people_holding_hands_tone4_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'people holding hands: medium-dark skin tone, medium skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}\u{1F3FD}', - shortName: 'people_holding_hands_tone4_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'people holding hands: medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}\u{1F3FE}', - shortName: 'people_holding_hands_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'people holding hands: medium-dark skin tone, dark skin tone', - char: '\u{1F9D1}\u{1F3FE}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}\u{1F3FF}', - shortName: 'people_holding_hands_tone4_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'people holding hands: dark skin tone, light skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}\u{1F3FB}', - shortName: 'people_holding_hands_tone5_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'people holding hands: dark skin tone, medium-light skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}\u{1F3FC}', - shortName: 'people_holding_hands_tone5_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'people holding hands: dark skin tone, medium skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}\u{1F3FD}', - shortName: 'people_holding_hands_tone5_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'people holding hands: dark skin tone, medium-dark skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}\u{1F3FE}', - shortName: 'people_holding_hands_tone5_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'people holding hands: dark skin tone', - char: '\u{1F9D1}\u{1F3FF}\u{200D}\u{1F91D}\u{200D}\u{1F9D1}\u{1F3FF}', - shortName: 'people_holding_hands_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'lgbt', - 'friend', - 'human', - 'daddy', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'woman and man holding hands', - char: '\u{1F46B}', - shortName: 'couple', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'couple', - 'hand', - 'hold', - 'man', - 'woman', - 'uc6', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ]), - Emoji( - name: 'woman and man holding hands: light skin tone', - char: '\u{1F46B}\u{1F3FB}', - shortName: 'woman_and_man_holding_hands_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: - 'woman and man holding hands: light skin tone, medium-light skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FC}', - shortName: 'woman_and_man_holding_hands_tone1_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'woman and man holding hands: light skin tone, medium skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FD}', - shortName: 'woman_and_man_holding_hands_tone1_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: - 'woman and man holding hands: light skin tone, medium-dark skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FE}', - shortName: 'woman_and_man_holding_hands_tone1_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'woman and man holding hands: light skin tone, dark skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FF}', - shortName: 'woman_and_man_holding_hands_tone1_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: - 'woman and man holding hands: medium-light skin tone, light skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FB}', - shortName: 'woman_and_man_holding_hands_tone2_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'woman and man holding hands: medium-light skin tone', - char: '\u{1F46B}\u{1F3FC}', - shortName: 'woman_and_man_holding_hands_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: - 'woman and man holding hands: medium-light skin tone, medium skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FD}', - shortName: 'woman_and_man_holding_hands_tone2_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: - 'woman and man holding hands: medium-light skin tone, medium-dark skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FE}', - shortName: 'woman_and_man_holding_hands_tone2_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: - 'woman and man holding hands: medium-light skin tone, dark skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FF}', - shortName: 'woman_and_man_holding_hands_tone2_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'woman and man holding hands: medium skin tone, light skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FB}', - shortName: 'woman_and_man_holding_hands_tone3_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: - 'woman and man holding hands: medium skin tone, medium-light skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FC}', - shortName: 'woman_and_man_holding_hands_tone3_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'woman and man holding hands: medium skin tone', - char: '\u{1F46B}\u{1F3FD}', - shortName: 'woman_and_man_holding_hands_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: - 'woman and man holding hands: medium skin tone, medium-dark skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FE}', - shortName: 'woman_and_man_holding_hands_tone3_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'woman and man holding hands: medium skin tone, dark skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FF}', - shortName: 'woman_and_man_holding_hands_tone3_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: - 'woman and man holding hands: medium-dark skin tone, light skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FB}', - shortName: 'woman_and_man_holding_hands_tone4_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: - 'woman and man holding hands: medium-dark skin tone, medium-light skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FC}', - shortName: 'woman_and_man_holding_hands_tone4_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: - 'woman and man holding hands: medium-dark skin tone, medium skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FD}', - shortName: 'woman_and_man_holding_hands_tone4_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'woman and man holding hands: medium-dark skin tone', - char: '\u{1F46B}\u{1F3FE}', - shortName: 'woman_and_man_holding_hands_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: - 'woman and man holding hands: medium-dark skin tone, dark skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FF}', - shortName: 'woman_and_man_holding_hands_tone4_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'woman and man holding hands: dark skin tone, light skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FB}', - shortName: 'woman_and_man_holding_hands_tone5_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: - 'woman and man holding hands: dark skin tone, medium-light skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FC}', - shortName: 'woman_and_man_holding_hands_tone5_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'woman and man holding hands: dark skin tone, medium skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FD}', - shortName: 'woman_and_man_holding_hands_tone5_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: - 'woman and man holding hands: dark skin tone, medium-dark skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FE}', - shortName: 'woman_and_man_holding_hands_tone5_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'woman and man holding hands: dark skin tone', - char: '\u{1F46B}\u{1F3FF}', - shortName: 'woman_and_man_holding_hands_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'creationism', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'adam & eve', - 'adam and eve', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'women holding hands', - char: '\u{1F46D}', - shortName: 'two_women_holding_hands', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'couple', - 'hand', - 'hold', - 'woman', - 'uc6', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'women holding hands: light skin tone', - char: '\u{1F46D}\u{1F3FB}', - shortName: 'women_holding_hands_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'women holding hands: light skin tone, medium-light skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F91D}\u{200D}\u{1F469}\u{1F3FC}', - shortName: 'women_holding_hands_tone1_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'women holding hands: light skin tone, medium skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F91D}\u{200D}\u{1F469}\u{1F3FD}', - shortName: 'women_holding_hands_tone1_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'women holding hands: light skin tone, medium-dark skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F91D}\u{200D}\u{1F469}\u{1F3FE}', - shortName: 'women_holding_hands_tone1_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'women holding hands: light skin tone, dark skin tone', - char: '\u{1F469}\u{1F3FB}\u{200D}\u{1F91D}\u{200D}\u{1F469}\u{1F3FF}', - shortName: 'women_holding_hands_tone1_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'women holding hands: medium-light skin tone, light skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F91D}\u{200D}\u{1F469}\u{1F3FB}', - shortName: 'women_holding_hands_tone2_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'women holding hands: medium-light skin tone', - char: '\u{1F46D}\u{1F3FC}', - shortName: 'women_holding_hands_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'women holding hands: medium-light skin tone, medium skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F91D}\u{200D}\u{1F469}\u{1F3FD}', - shortName: 'women_holding_hands_tone2_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: - 'women holding hands: medium-light skin tone, medium-dark skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F91D}\u{200D}\u{1F469}\u{1F3FE}', - shortName: 'women_holding_hands_tone2_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'women holding hands: medium-light skin tone, dark skin tone', - char: '\u{1F469}\u{1F3FC}\u{200D}\u{1F91D}\u{200D}\u{1F469}\u{1F3FF}', - shortName: 'women_holding_hands_tone2_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'women holding hands: medium skin tone, light skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F91D}\u{200D}\u{1F469}\u{1F3FB}', - shortName: 'women_holding_hands_tone3_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'women holding hands: medium skin tone, medium-light skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F91D}\u{200D}\u{1F469}\u{1F3FC}', - shortName: 'women_holding_hands_tone3_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'women holding hands: medium skin tone', - char: '\u{1F46D}\u{1F3FD}', - shortName: 'women_holding_hands_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'women holding hands: medium skin tone, medium-dark skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F91D}\u{200D}\u{1F469}\u{1F3FE}', - shortName: 'women_holding_hands_tone3_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'women holding hands: medium skin tone, dark skin tone', - char: '\u{1F469}\u{1F3FD}\u{200D}\u{1F91D}\u{200D}\u{1F469}\u{1F3FF}', - shortName: 'women_holding_hands_tone3_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'women holding hands: medium-dark skin tone, light skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F91D}\u{200D}\u{1F469}\u{1F3FB}', - shortName: 'women_holding_hands_tone4_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: - 'women holding hands: medium-dark skin tone, medium-light skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F91D}\u{200D}\u{1F469}\u{1F3FC}', - shortName: 'women_holding_hands_tone4_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'women holding hands: medium-dark skin tone, medium skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F91D}\u{200D}\u{1F469}\u{1F3FD}', - shortName: 'women_holding_hands_tone4_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'women holding hands: medium-dark skin tone', - char: '\u{1F46D}\u{1F3FE}', - shortName: 'women_holding_hands_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'women holding hands: medium-dark skin tone, dark skin tone', - char: '\u{1F469}\u{1F3FE}\u{200D}\u{1F91D}\u{200D}\u{1F469}\u{1F3FF}', - shortName: 'women_holding_hands_tone4_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'women holding hands: dark skin tone, light skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F91D}\u{200D}\u{1F469}\u{1F3FB}', - shortName: 'women_holding_hands_tone5_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'women holding hands: dark skin tone, medium-light skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F91D}\u{200D}\u{1F469}\u{1F3FC}', - shortName: 'women_holding_hands_tone5_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'women holding hands: dark skin tone, medium skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F91D}\u{200D}\u{1F469}\u{1F3FD}', - shortName: 'women_holding_hands_tone5_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'women holding hands: dark skin tone, medium-dark skin tone', - char: '\u{1F469}\u{1F3FF}\u{200D}\u{1F91D}\u{200D}\u{1F469}\u{1F3FE}', - shortName: 'women_holding_hands_tone5_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'women holding hands: dark skin tone', - char: '\u{1F46D}\u{1F3FF}', - shortName: 'women_holding_hands_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'girls night', - 'friend', - 'human', - 'porn', - 'parent', - 'wife', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'gender', - 'people', - 'parents', - 'adult', - 'maman', - 'mommy', - 'mama', - 'mother' - ], - modifiable: true), - Emoji( - name: 'men holding hands', - char: '\u{1F46C}', - shortName: 'two_men_holding_hands', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'Gemini', - 'couple', - 'hand', - 'hold', - 'man', - 'twins', - 'zodiac', - 'uc6', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ]), - Emoji( - name: 'men holding hands: light skin tone', - char: '\u{1F46C}\u{1F3FB}', - shortName: 'men_holding_hands_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'men holding hands: light skin tone, medium-light skin tone', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FC}', - shortName: 'men_holding_hands_tone1_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'men holding hands: light skin tone, medium skin tone', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FD}', - shortName: 'men_holding_hands_tone1_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'men holding hands: light skin tone, medium-dark skin tone', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FE}', - shortName: 'men_holding_hands_tone1_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'men holding hands: light skin tone, dark skin tone', - char: '\u{1F468}\u{1F3FB}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FF}', - shortName: 'men_holding_hands_tone1_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'men holding hands: medium-light skin tone, light skin tone', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FB}', - shortName: 'men_holding_hands_tone2_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'men holding hands: medium-light skin tone', - char: '\u{1F46C}\u{1F3FC}', - shortName: 'men_holding_hands_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'men holding hands: medium-light skin tone, medium skin tone', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FD}', - shortName: 'men_holding_hands_tone2_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'men holding hands: medium-light skin tone, medium-dark skin tone', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FE}', - shortName: 'men_holding_hands_tone2_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'men holding hands: medium-light skin tone, dark skin tone', - char: '\u{1F468}\u{1F3FC}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FF}', - shortName: 'men_holding_hands_tone2_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'men holding hands: medium skin tone, light skin tone', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FB}', - shortName: 'men_holding_hands_tone3_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'men holding hands: medium skin tone, medium-light skin tone', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FC}', - shortName: 'men_holding_hands_tone3_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'men holding hands: medium skin tone', - char: '\u{1F46C}\u{1F3FD}', - shortName: 'men_holding_hands_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'men holding hands: medium skin tone, medium-dark skin tone', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FE}', - shortName: 'men_holding_hands_tone3_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'men holding hands: medium skin tone, dark skin tone', - char: '\u{1F468}\u{1F3FD}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FF}', - shortName: 'men_holding_hands_tone3_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'men holding hands: medium-dark skin tone, light skin tone', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FB}', - shortName: 'men_holding_hands_tone4_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'men holding hands: medium-dark skin tone, medium-light skin tone', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FC}', - shortName: 'men_holding_hands_tone4_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'men holding hands: medium-dark skin tone, medium skin tone', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FD}', - shortName: 'men_holding_hands_tone4_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'men holding hands: medium-dark skin tone', - char: '\u{1F46C}\u{1F3FE}', - shortName: 'men_holding_hands_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'men holding hands: medium-dark skin tone, dark skin tone', - char: '\u{1F468}\u{1F3FE}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FF}', - shortName: 'men_holding_hands_tone4_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'men holding hands: dark skin tone, light skin tone', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FB}', - shortName: 'men_holding_hands_tone5_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'men holding hands: dark skin tone, medium-light skin tone', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FC}', - shortName: 'men_holding_hands_tone5_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'men holding hands: dark skin tone, medium skin tone', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FD}', - shortName: 'men_holding_hands_tone5_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'men holding hands: dark skin tone, medium-dark skin tone', - char: '\u{1F468}\u{1F3FF}\u{200D}\u{1F91D}\u{200D}\u{1F468}\u{1F3FE}', - shortName: 'men_holding_hands_tone5_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'men holding hands: dark skin tone', - char: '\u{1F46C}\u{1F3FF}', - shortName: 'men_holding_hands_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'uc12', - 'family', - 'diversity', - 'wedding', - 'gay', - 'men', - 'lgbt', - 'friend', - 'queen', - 'human', - 'daddy', - 'porn', - 'parent', - 'husband', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'king', - 'prince', - 'princess', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult' - ], - modifiable: true), - Emoji( - name: 'couple with heart', - char: '\u{1F491}', - shortName: 'couple_with_heart', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'couple', - 'love', - 'uc6', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'love', - 'sex', - 'lgbt', - 'pink', - 'husband', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'fuck', - 'fucking', - 'horny', - 'humping', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'rose' - ]), - Emoji( - name: 'couple with heart: woman, man', - char: '\u{1F469}\u{200D}\u{2764}\u{FE0F}\u{200D}\u{1F468}', - shortName: 'couple_with_heart_woman_man', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'couple', - 'love', - 'man', - 'woman', - 'uc6', - 'wedding', - 'love', - 'sex', - 'husband', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'fuck', - 'fucking', - 'horny', - 'humping' - ]), - Emoji( - name: 'couple with heart: woman, woman', - char: '\u{1F469}\u{200D}\u{2764}\u{FE0F}\u{200D}\u{1F469}', - shortName: 'couple_ww', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'couple', - 'love', - 'woman', - 'uc6', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'love', - 'sex', - 'lgbt', - 'pink', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'fuck', - 'fucking', - 'horny', - 'humping', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'rose' - ]), - Emoji( - name: 'couple with heart: man, man', - char: '\u{1F468}\u{200D}\u{2764}\u{FE0F}\u{200D}\u{1F468}', - shortName: 'couple_mm', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'couple', - 'love', - 'man', - 'uc6', - 'wedding', - 'gay', - 'men', - 'love', - 'sex', - 'lgbt', - 'pink', - 'husband', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'fuck', - 'fucking', - 'horny', - 'humping', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'rose' - ]), - Emoji( - name: 'kiss', - char: '\u{1F48F}', - shortName: 'couplekiss', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'couple', - 'uc6', - 'wedding', - 'lesbian', - 'gay', - 'men', - 'love', - 'sex', - 'hug', - 'lgbt', - 'pink', - 'kisses', - 'husband', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'fuck', - 'fucking', - 'horny', - 'humping', - 'embrace', - 'hugs', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'rose', - 'bisous', - 'beijos', - 'besos', - 'bise', - 'blowing kisses', - 'kissy' - ]), - Emoji( - name: 'kiss: woman, man', - char: - '\u{1F469}\u{200D}\u{2764}\u{FE0F}\u{200D}\u{1F48B}\u{200D}\u{1F468}', - shortName: 'kiss_woman_man', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'couple', - 'man', - 'woman', - 'uc6', - 'wedding', - 'love', - 'sex', - 'hug', - 'kisses', - 'husband', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'fuck', - 'fucking', - 'horny', - 'humping', - 'embrace', - 'hugs', - 'bisous', - 'beijos', - 'besos', - 'bise', - 'blowing kisses', - 'kissy' - ]), - Emoji( - name: 'kiss: woman, woman', - char: - '\u{1F469}\u{200D}\u{2764}\u{FE0F}\u{200D}\u{1F48B}\u{200D}\u{1F469}', - shortName: 'kiss_ww', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'couple', - 'woman', - 'uc6', - 'wedding', - 'lesbian', - 'gay', - 'women', - 'love', - 'sex', - 'hug', - 'lgbt', - 'pink', - 'kisses', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'woman', - 'female', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'fuck', - 'fucking', - 'horny', - 'humping', - 'embrace', - 'hugs', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'rose', - 'bisous', - 'beijos', - 'besos', - 'bise', - 'blowing kisses', - 'kissy' - ]), - Emoji( - name: 'kiss: man, man', - char: - '\u{1F468}\u{200D}\u{2764}\u{FE0F}\u{200D}\u{1F48B}\u{200D}\u{1F468}', - shortName: 'kiss_mm', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'couple', - 'man', - 'uc6', - 'wedding', - 'gay', - 'men', - 'love', - 'sex', - 'hug', - 'lgbt', - 'pink', - 'kisses', - 'husband', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'fuck', - 'fucking', - 'horny', - 'humping', - 'embrace', - 'hugs', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'rose', - 'bisous', - 'beijos', - 'besos', - 'bise', - 'blowing kisses', - 'kissy' - ]), - Emoji( - name: 'family', - char: '\u{1F46A}', - shortName: 'family', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'family', - 'uc6', - 'family', - 'lesbian', - 'gay', - 'men', - 'christmas', - 'lgbt', - 'human', - 'daddy', - 'parent', - 'wife', - 'husband', - 'child', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'family: man, woman, boy', - char: '\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F466}', - shortName: 'family_man_woman_boy', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'boy', - 'family', - 'man', - 'woman', - 'uc6', - 'family', - 'human', - 'daddy', - 'parent', - 'wife', - 'husband', - 'child', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'family: man, woman, girl', - char: '\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}', - shortName: 'family_mwg', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'family', - 'girl', - 'man', - 'woman', - 'uc6', - 'family', - 'human', - 'daddy', - 'parent', - 'wife', - 'husband', - 'child', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'family: man, woman, girl, boy', - char: '\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}', - shortName: 'family_mwgb', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'boy', - 'family', - 'girl', - 'man', - 'woman', - 'uc6', - 'family', - 'human', - 'daddy', - 'parent', - 'wife', - 'husband', - 'child', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'family: man, woman, boy, boy', - char: '\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F466}\u{200D}\u{1F466}', - shortName: 'family_mwbb', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'boy', - 'family', - 'man', - 'woman', - 'uc6', - 'family', - 'human', - 'daddy', - 'parent', - 'wife', - 'husband', - 'child', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'family: man, woman, girl, girl', - char: '\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F467}', - shortName: 'family_mwgg', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'family', - 'girl', - 'man', - 'woman', - 'uc6', - 'family', - 'human', - 'daddy', - 'parent', - 'wife', - 'husband', - 'child', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'family: woman, woman, boy', - char: '\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F466}', - shortName: 'family_wwb', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'boy', - 'family', - 'woman', - 'uc6', - 'family', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'human', - 'parent', - 'wife', - 'child', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'gender', - 'people', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'family: woman, woman, girl', - char: '\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}', - shortName: 'family_wwg', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'family', - 'girl', - 'woman', - 'uc6', - 'family', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'human', - 'parent', - 'wife', - 'child', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'gender', - 'people', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'family: woman, woman, girl, boy', - char: '\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}', - shortName: 'family_wwgb', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'boy', - 'family', - 'girl', - 'woman', - 'uc6', - 'family', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'human', - 'parent', - 'wife', - 'child', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'gender', - 'people', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'family: woman, woman, boy, boy', - char: '\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F466}\u{200D}\u{1F466}', - shortName: 'family_wwbb', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'boy', - 'family', - 'woman', - 'uc6', - 'family', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'human', - 'parent', - 'wife', - 'child', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'gender', - 'people', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'family: woman, woman, girl, girl', - char: '\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F467}', - shortName: 'family_wwgg', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'family', - 'girl', - 'woman', - 'uc6', - 'family', - 'lesbian', - 'gay', - 'women', - 'lgbt', - 'human', - 'parent', - 'wife', - 'child', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'twink', - 'woman', - 'female', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'gender', - 'people', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'family: man, man, boy', - char: '\u{1F468}\u{200D}\u{1F468}\u{200D}\u{1F466}', - shortName: 'family_mmb', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'boy', - 'family', - 'man', - 'uc6', - 'family', - 'gay', - 'men', - 'lgbt', - 'human', - 'daddy', - 'parent', - 'husband', - 'child', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ]), - Emoji( - name: 'family: man, man, girl', - char: '\u{1F468}\u{200D}\u{1F468}\u{200D}\u{1F467}', - shortName: 'family_mmg', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'family', - 'girl', - 'man', - 'uc6', - 'family', - 'gay', - 'men', - 'lgbt', - 'human', - 'daddy', - 'parent', - 'husband', - 'child', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ]), - Emoji( - name: 'family: man, man, girl, boy', - char: '\u{1F468}\u{200D}\u{1F468}\u{200D}\u{1F467}\u{200D}\u{1F466}', - shortName: 'family_mmgb', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'boy', - 'family', - 'girl', - 'man', - 'uc6', - 'family', - 'gay', - 'men', - 'lgbt', - 'human', - 'daddy', - 'parent', - 'husband', - 'child', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ]), - Emoji( - name: 'family: man, man, boy, boy', - char: '\u{1F468}\u{200D}\u{1F468}\u{200D}\u{1F466}\u{200D}\u{1F466}', - shortName: 'family_mmbb', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'boy', - 'family', - 'man', - 'uc6', - 'family', - 'gay', - 'men', - 'lgbt', - 'human', - 'daddy', - 'parent', - 'husband', - 'child', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ]), - Emoji( - name: 'family: man, man, girl, girl', - char: '\u{1F468}\u{200D}\u{1F468}\u{200D}\u{1F467}\u{200D}\u{1F467}', - shortName: 'family_mmgg', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'family', - 'girl', - 'man', - 'uc6', - 'family', - 'gay', - 'men', - 'lgbt', - 'human', - 'daddy', - 'parent', - 'husband', - 'child', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'twink', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ]), - Emoji( - name: 'family: woman, boy', - char: '\u{1F469}\u{200D}\u{1F466}', - shortName: 'family_woman_boy', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'boy', - 'family', - 'woman', - 'uc6', - 'family', - 'women', - 'human', - 'parent', - 'wife', - 'child', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'woman', - 'female', - 'gender', - 'people', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'family: woman, girl', - char: '\u{1F469}\u{200D}\u{1F467}', - shortName: 'family_woman_girl', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'family', - 'girl', - 'woman', - 'uc6', - 'family', - 'women', - 'human', - 'parent', - 'wife', - 'child', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'woman', - 'female', - 'gender', - 'people', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'family: woman, girl, boy', - char: '\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}', - shortName: 'family_woman_girl_boy', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'boy', - 'family', - 'girl', - 'woman', - 'uc6', - 'family', - 'women', - 'human', - 'parent', - 'wife', - 'child', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'woman', - 'female', - 'gender', - 'people', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'family: woman, boy, boy', - char: '\u{1F469}\u{200D}\u{1F466}\u{200D}\u{1F466}', - shortName: 'family_woman_boy_boy', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'boy', - 'family', - 'woman', - 'uc6', - 'family', - 'women', - 'human', - 'parent', - 'wife', - 'child', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'woman', - 'female', - 'gender', - 'people', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'family: woman, girl, girl', - char: '\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F467}', - shortName: 'family_woman_girl_girl', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'family', - 'girl', - 'woman', - 'uc6', - 'family', - 'women', - 'human', - 'parent', - 'wife', - 'child', - 'mom', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'woman', - 'female', - 'gender', - 'people', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'family: man, boy', - char: '\u{1F468}\u{200D}\u{1F466}', - shortName: 'family_man_boy', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'boy', - 'family', - 'man', - 'uc6', - 'family', - 'men', - 'human', - 'daddy', - 'parent', - 'husband', - 'child', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ]), - Emoji( - name: 'family: man, girl', - char: '\u{1F468}\u{200D}\u{1F467}', - shortName: 'family_man_girl', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'family', - 'girl', - 'man', - 'uc6', - 'family', - 'men', - 'human', - 'daddy', - 'parent', - 'husband', - 'child', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ]), - Emoji( - name: 'family: man, girl, boy', - char: '\u{1F468}\u{200D}\u{1F467}\u{200D}\u{1F466}', - shortName: 'family_man_girl_boy', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'boy', - 'family', - 'girl', - 'man', - 'uc6', - 'family', - 'men', - 'human', - 'daddy', - 'parent', - 'husband', - 'child', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ]), - Emoji( - name: 'family: man, boy, boy', - char: '\u{1F468}\u{200D}\u{1F466}\u{200D}\u{1F466}', - shortName: 'family_man_boy_boy', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'boy', - 'family', - 'man', - 'uc6', - 'family', - 'men', - 'human', - 'daddy', - 'parent', - 'husband', - 'child', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ]), - Emoji( - name: 'family: man, girl, girl', - char: '\u{1F468}\u{200D}\u{1F467}\u{200D}\u{1F467}', - shortName: 'family_man_girl_girl', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.family, - keywords: [ - 'family', - 'girl', - 'man', - 'uc6', - 'family', - 'men', - 'human', - 'daddy', - 'parent', - 'husband', - 'child', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'gender', - 'people', - 'dad', - 'papa', - 'pere', - 'father', - 'parents', - 'adult', - 'children', - 'girl', - 'boy', - 'kids', - 'niño', - 'enfant' - ]), - Emoji( - name: 'yarn', - char: '\u{1F9F6}', - shortName: 'yarn', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.artsCrafts, - keywords: [ - 'uc11', - 'cat', - 'household', - 'sew', - 'kitty', - 'kitten', - 'cats', - 'kittens', - 'kitties', - 'feline', - 'felines', - 'cat face', - 'gato', - 'meow', - 'knit', - 'embroider', - 'stitch', - 'repair', - 'crochet', - 'alter', - 'seamstress', - 'fix' - ]), - Emoji( - name: 'thread', - char: '\u{1F9F5}', - shortName: 'thread', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.artsCrafts, - keywords: [ - 'uc11', - 'household', - 'sew', - 'knit', - 'embroider', - 'stitch', - 'repair', - 'crochet', - 'alter', - 'seamstress', - 'fix' - ]), - Emoji( - name: 'coat', - char: '\u{1F9E5}', - shortName: 'coat', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'jacket', - 'uc10', - 'fashion', - 'winter', - 'cold', - 'jacket', - 'clothes', - 'clothing', - 'style', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles', - 'veste' - ]), - Emoji( - name: 'lab coat', - char: '\u{1F97C}', - shortName: 'lab_coat', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: ['uc11', 'science', 'jacket', 'medical', 'lab', 'veste']), - Emoji( - name: 'safety vest', - char: '\u{1F9BA}', - shortName: 'safety_vest', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'uc12', - '911', - 'jacket', - 'construction', - 'emergency', - 'injury', - 'veste' - ]), - Emoji( - name: 'woman’s clothes', - char: '\u{1F45A}', - shortName: 'womans_clothes', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'clothing', - 'woman', - 'uc6', - 'fashion', - 'women', - 'pink', - 'mom', - 'clothes', - 'clothing', - 'style', - 'woman', - 'female', - 'rose', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 't-shirt', - char: '\u{1F455}', - shortName: 'shirt', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'clothing', - 'shirt', - 'tshirt', - 'uc6', - 'fashion', - 'men', - 'clothes', - 'clothing', - 'style', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male' - ]), - Emoji( - name: 'jeans', - char: '\u{1F456}', - shortName: 'jeans', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'clothing', - 'pants', - 'trousers', - 'uc6', - 'fashion', - 'men', - 'pants', - 'clothes', - 'clothing', - 'style', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'pant', - 'levis', - 'slacks' - ]), - Emoji( - name: 'briefs', - char: '\u{1FA72}', - shortName: 'briefs', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'uc12', - 'fashion', - 'men', - 'underwear', - 'clothes', - 'clothing', - 'style', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'undergarments', - 'boxers', - 'panties', - 'boy shorts', - 'panty', - 'biancheria intima', - 'sous-vêtements', - 'ropa interior', - 'speedos' - ]), - Emoji( - name: 'shorts', - char: '\u{1FA73}', - shortName: 'shorts', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'uc12', - 'fashion', - 'vacation', - 'swim', - 'beach', - 'scuba', - 'pantalones cortos', - 'clothes', - 'clothing', - 'style', - 'swimming', - 'swimmer', - 'snorkel', - 'kurze Hose', - 'pantaloncini' - ]), - Emoji( - name: 'necktie', - char: '\u{1F454}', - shortName: 'necktie', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'clothing', - 'uc6', - 'fashion', - 'men', - 'accessories', - 'business', - 'clothes', - 'clothing', - 'style', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male' - ]), - Emoji( - name: 'dress', - char: '\u{1F457}', - shortName: 'dress', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'clothing', - 'uc6', - 'fashion', - 'women', - 'sexy', - 'beautiful', - 'girls night', - 'pink', - 'vintage', - 'florida', - 'mom', - 'dress', - 'clothes', - 'clothing', - 'style', - 'woman', - 'female', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'rose', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'bikini', - char: '\u{1F459}', - shortName: 'bikini', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'clothing', - 'swim', - 'uc6', - 'fashion', - 'women', - 'sexy', - 'tropical', - 'vacation', - 'swim', - 'beach', - 'hawaii', - 'california', - 'florida', - 'las vegas', - 'summer', - 'binoculars', - 'clothes', - 'clothing', - 'style', - 'woman', - 'female', - 'swimming', - 'swimmer', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'vegas', - 'weekend' - ]), - Emoji( - name: 'one-piece swimsuit', - char: '\u{1FA71}', - shortName: 'one_piece_swimsuit', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'uc12', - 'fashion', - 'tropical', - 'vacation', - 'swim', - 'beach', - 'hawaii', - 'california', - 'florida', - 'scuba', - 'summer', - 'clothes', - 'clothing', - 'style', - 'swimming', - 'swimmer', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'snorkel', - 'weekend' - ]), - Emoji( - name: 'kimono', - char: '\u{1F458}', - shortName: 'kimono', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'clothing', - 'uc6', - 'fashion', - 'japan', - 'pink', - 'dress', - 'clothes', - 'clothing', - 'style', - 'japanese', - 'ninja', - 'rose' - ]), - Emoji( - name: 'sari', - char: '\u{1F97B}', - shortName: 'sari', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'uc12', - 'fashion', - 'saree', - 'dress', - 'clothes', - 'clothing', - 'style', - 'shari', - 'nivi', - 'choli', - 'ravike', - 'cholo', - 'parkar', - 'ul-pavadai' - ]), - Emoji( - name: 'flat shoe', - char: '\u{1F97F}', - shortName: 'womans_flat_shoe', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'uc11', - 'fashion', - 'women', - 'shoe', - 'accessories', - 'pink', - 'mom', - 'clothes', - 'clothing', - 'style', - 'woman', - 'female', - 'shoes', - 'baskets', - 'loafers', - 'sandals', - 'pumps', - 'boots', - 'heels', - 'rose', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'high-heeled shoe', - char: '\u{1F460}', - shortName: 'high_heel', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'clothing', - 'heel', - 'shoe', - 'woman', - 'uc6', - 'fashion', - 'women', - 'shoe', - 'sexy', - 'accessories', - 'girls night', - 'california', - 'las vegas', - 'rich', - 'mom', - 'clothes', - 'clothing', - 'style', - 'woman', - 'female', - 'shoes', - 'baskets', - 'loafers', - 'sandals', - 'pumps', - 'boots', - 'heels', - 'ladies night', - 'girls only', - 'girlfriend', - 'vegas', - 'grand', - 'expensive', - 'fancy', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'woman’s sandal', - char: '\u{1F461}', - shortName: 'sandal', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'clothing', - 'sandal', - 'shoe', - 'woman', - 'uc6', - 'fashion', - 'women', - 'shoe', - 'accessories', - 'pink', - 'summer', - 'mom', - 'clothes', - 'clothing', - 'style', - 'woman', - 'female', - 'shoes', - 'baskets', - 'loafers', - 'sandals', - 'pumps', - 'boots', - 'heels', - 'rose', - 'weekend', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'woman’s boot', - char: '\u{1F462}', - shortName: 'boot', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'boot', - 'clothing', - 'shoe', - 'woman', - 'uc6', - 'fashion', - 'women', - 'shoe', - 'sexy', - 'accessories', - 'vintage', - 'rich', - 'clothes', - 'clothing', - 'style', - 'woman', - 'female', - 'shoes', - 'baskets', - 'loafers', - 'sandals', - 'pumps', - 'boots', - 'heels', - 'grand', - 'expensive', - 'fancy' - ]), - Emoji( - name: 'man’s shoe', - char: '\u{1F45E}', - shortName: 'mans_shoe', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'clothing', - 'man', - 'shoe', - 'uc6', - 'fashion', - 'shoe', - 'men', - 'accessories', - 'vintage', - 'clothes', - 'clothing', - 'style', - 'shoes', - 'baskets', - 'loafers', - 'sandals', - 'pumps', - 'boots', - 'heels', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male' - ]), - Emoji( - name: 'running shoe', - char: '\u{1F45F}', - shortName: 'athletic_shoe', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'athletic', - 'clothing', - 'shoe', - 'sneaker', - 'uc6', - 'sport', - 'fashion', - 'shoe', - 'accessories', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'clothes', - 'clothing', - 'style', - 'shoes', - 'baskets', - 'loafers', - 'sandals', - 'pumps', - 'boots', - 'heels' - ]), - Emoji( - name: 'hiking boot', - char: '\u{1F97E}', - shortName: 'hiking_boot', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'uc11', - 'shoe', - 'accessories', - 'mountain', - 'activity', - 'rock climbing', - 'shoes', - 'baskets', - 'loafers', - 'sandals', - 'pumps', - 'boots', - 'heels', - 'climber' - ]), - Emoji( - name: 'thong sandal', - char: '\u{1FA74}', - shortName: 'thong_sandal', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'uc13', - 'tropical', - 'beach', - 'hawaii', - 'flip flop', - 'california', - 'florida', - 'summer', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'weekend' - ]), - Emoji( - name: 'socks', - char: '\u{1F9E6}', - shortName: 'socks', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'stocking', - 'uc10', - 'fashion', - 'winter', - 'cold', - 'accessories', - 'clothes', - 'clothing', - 'style', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles' - ]), - Emoji( - name: 'gloves', - char: '\u{1F9E4}', - shortName: 'gloves', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'hand', - 'uc10', - 'fashion', - 'winter', - 'cold', - 'accessories', - 'gloves', - 'mittins', - 'clothes', - 'clothing', - 'style', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles', - 'muff' - ]), - Emoji( - name: 'scarf', - char: '\u{1F9E3}', - shortName: 'scarf', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'neck', - 'uc10', - 'fashion', - 'winter', - 'cold', - 'accessories', - 'clothes', - 'clothing', - 'style', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles' - ]), - Emoji( - name: 'top hat', - char: '\u{1F3A9}', - shortName: 'tophat', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'clothing', - 'hat', - 'top', - 'tophat', - 'uc6', - 'fashion', - 'wedding', - 'hat', - 'men', - 'accessories', - 'magic', - 'vintage', - 'rich', - 'clothes', - 'clothing', - 'style', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'hats', - 'cap', - 'caps', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'spell', - 'genie', - 'magical', - 'grand', - 'expensive', - 'fancy' - ]), - Emoji( - name: 'billed cap', - char: '\u{1F9E2}', - shortName: 'billed_cap', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'baseball cap', - 'uc10', - 'fashion', - 'hat', - 'accessories', - 'clothes', - 'clothing', - 'style', - 'hats', - 'cap', - 'caps' - ]), - Emoji( - name: 'woman’s hat', - char: '\u{1F452}', - shortName: 'womans_hat', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'clothing', - 'hat', - 'woman', - 'uc6', - 'fashion', - 'hat', - 'women', - 'accessories', - 'pink', - 'vintage', - 'easter', - 'rich', - 'clothes', - 'clothing', - 'style', - 'hats', - 'cap', - 'caps', - 'woman', - 'female', - 'rose', - 'grand', - 'expensive', - 'fancy' - ]), - Emoji( - name: 'graduation cap', - char: '\u{1F393}', - shortName: 'mortar_board', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'cap', - 'celebration', - 'clothing', - 'graduation', - 'hat', - 'uc6', - 'hat', - 'classroom', - 'accessories', - 'graduate', - 'hats', - 'cap', - 'caps', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning' - ]), - Emoji( - name: 'rescue worker’s helmet', - char: '\u{26D1}\u{FE0F}', - shortName: 'helmet_with_cross', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'aid', - 'cross', - 'face', - 'hat', - 'helmet', - 'uc5', - 'hat', - 'accessories', - 'job', - '911', - 'help', - 'helmet', - 'hats', - 'cap', - 'caps', - 'profession', - 'boss', - 'career', - 'emergency', - 'injury' - ]), - Emoji( - name: 'military helmet', - char: '\u{1FA96}', - shortName: 'military_helmet', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'uc13', - 'hat', - 'soldier', - 'helmet', - 'army', - 'hats', - 'cap', - 'caps' - ]), - Emoji( - name: 'crown', - char: '\u{1F451}', - shortName: 'crown', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'clothing', - 'king', - 'queen', - 'uc6', - 'accessories', - 'power', - 'queen', - 'england', - 'bling', - 'fame', - 'crown', - 'rich', - 'king', - 'prince', - 'princess', - 'united kingdom', - 'london', - 'uk', - 'jewels', - 'gems', - 'jewel', - 'jewelry', - 'treasure', - 'famous', - 'celebrity', - 'tiara', - 'grand', - 'expensive', - 'fancy' - ]), - Emoji( - name: 'ring', - char: '\u{1F48D}', - shortName: 'ring', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'diamond', - 'uc6', - 'wedding', - 'accessories', - 'girls night', - 'trap', - 'vintage', - 'bling', - 'diamond', - 'rich', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'ladies night', - 'girls only', - 'girlfriend', - 'jewels', - 'gems', - 'jewel', - 'jewelry', - 'treasure', - 'grand', - 'expensive', - 'fancy' - ]), - Emoji( - name: 'clutch bag', - char: '\u{1F45D}', - shortName: 'pouch', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'bag', - 'clothing', - 'pouch', - 'uc6', - 'fashion', - 'women', - 'bag', - 'accessories', - 'mom', - 'clothes', - 'clothing', - 'style', - 'woman', - 'female', - 'swag', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'purse', - char: '\u{1F45B}', - shortName: 'purse', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'clothing', - 'coin', - 'uc6', - 'fashion', - 'women', - 'bag', - 'money', - 'accessories', - 'pink', - 'vintage', - 'mom', - 'clothes', - 'clothing', - 'style', - 'woman', - 'female', - 'swag', - 'cash', - 'dollars', - 'dollar', - 'bucks', - 'currency', - 'funds', - 'payment', - 'money face', - 'reward', - 'thief', - 'bank', - 'benjamins', - 'argent', - 'dinero', - 'i soldi', - 'Geld', - 'rose', - 'maman', - 'mommy', - 'mama', - 'mother' - ]), - Emoji( - name: 'handbag', - char: '\u{1F45C}', - shortName: 'handbag', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'bag', - 'clothing', - 'purse', - 'uc6', - 'fashion', - 'women', - 'bag', - 'vacation', - 'accessories', - 'rich', - 'mom', - 'work', - 'clothes', - 'clothing', - 'style', - 'woman', - 'female', - 'swag', - 'grand', - 'expensive', - 'fancy', - 'maman', - 'mommy', - 'mama', - 'mother', - 'office' - ]), - Emoji( - name: 'briefcase', - char: '\u{1F4BC}', - shortName: 'briefcase', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.office, - keywords: [ - 'briefcase', - 'uc6', - 'fashion', - 'bag', - 'men', - 'classroom', - 'accessories', - 'nutcase', - 'job', - 'business', - 'rich', - 'work', - 'clothes', - 'clothing', - 'style', - 'swag', - 'man', - 'guy', - 'guys', - 'gentleman', - 'male', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'profession', - 'boss', - 'career', - 'grand', - 'expensive', - 'fancy', - 'office' - ]), - Emoji( - name: 'backpack', - char: '\u{1F392}', - shortName: 'school_satchel', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'bag', - 'satchel', - 'school', - 'uc6', - 'fashion', - 'bag', - 'classroom', - 'vacation', - 'accessories', - 'backpack', - 'suitcase', - 'clothes', - 'clothing', - 'style', - 'swag', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'carry-on' - ]), - Emoji( - name: 'luggage', - char: '\u{1F9F3}', - shortName: 'luggage', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.hotel, - keywords: [ - 'uc11', - 'bag', - 'travel', - 'vacation', - 'suitcase', - 'household', - 'swag', - 'carry-on' - ]), - Emoji( - name: 'glasses', - char: '\u{1F453}', - shortName: 'eyeglasses', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'clothing', - 'eye', - 'eyeglasses', - 'eyewear', - 'uc6', - 'fashion', - 'glasses', - 'accessories', - 'harry potter', - 'detective', - 'clothes', - 'clothing', - 'style', - 'eyeglasses', - 'eye glasses' - ]), - Emoji( - name: 'sunglasses', - char: '\u{1F576}\u{FE0F}', - shortName: 'dark_sunglasses', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'dark', - 'eye', - 'eyewear', - 'glasses', - 'uc7', - 'fashion', - 'glasses', - 'accessories', - 'awesome', - 'beautiful', - 'sunglasses', - 'hawaii', - 'california', - 'florida', - 'las vegas', - 'summer', - 'clothes', - 'clothing', - 'style', - 'eyeglasses', - 'eye glasses', - 'okay', - 'got it', - 'cool', - 'ok', - 'will do', - 'like', - 'bien', - 'yep', - 'yup', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'shades', - 'lunettes de soleil', - 'sun glasses', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'vegas', - 'weekend' - ]), - Emoji( - name: 'goggles', - char: '\u{1F97D}', - shortName: 'goggles', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'uc11', - 'glasses', - 'science', - 'accessories', - 'medical', - 'eyeglasses', - 'eye glasses', - 'lab' - ]), - Emoji( - name: 'closed umbrella', - char: '\u{1F302}', - shortName: 'closed_umbrella', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'clothing', - 'rain', - 'umbrella', - 'uc6', - 'sky', - 'rain', - 'accessories', - 'umbrella', - 'england', - 'cane', - 'united kingdom', - 'london', - 'uk' - ]), - Emoji( - name: 'curly hair', - char: '\u{1F9B1}', - shortName: 'curly_haired', - emojiGroup: EmojiGroup.component, - emojiSubgroup: EmojiSubgroup.hairStyle, - keywords: [ - 'uc11', - 'diversity', - 'body', - 'hair', - 'afro', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - "'fro", - 'curls', - 'frizzy', - 'perm' - ]), - Emoji( - name: 'red hair', - char: '\u{1F9B0}', - shortName: 'red_haired', - emojiGroup: EmojiGroup.component, - emojiSubgroup: EmojiSubgroup.hairStyle, - keywords: [ - 'uc11', - 'diversity', - 'body', - 'hair', - 'ginger', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy' - ]), - Emoji( - name: 'white hair', - char: '\u{1F9B3}', - shortName: 'white_haired', - emojiGroup: EmojiGroup.component, - emojiSubgroup: EmojiSubgroup.hairStyle, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'body', - 'hair', - 'grey hair', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'silver hair' - ]), - Emoji( - name: 'bald', - char: '\u{1F9B2}', - shortName: 'bald', - emojiGroup: EmojiGroup.component, - emojiSubgroup: EmojiSubgroup.hairStyle, - keywords: [ - 'uc11', - 'old people', - 'diversity', - 'body', - 'shaved head', - 'hairless', - 'grandparents', - 'elderly', - 'grandma', - 'grandpa', - 'grandmother', - 'grandfather', - 'mamie', - 'papy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'body part', - 'anatomy', - 'balding' - ]), - Emoji( - name: 'dog face', - char: '\u{1F436}', - shortName: 'dog', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'dog', - 'face', - 'pet', - 'uc6', - 'animal', - 'dog', - 'pug', - 'harry potter', - 'bingo', - 'bitch', - 'pets', - 'pokemon', - 'minecraft', - 'animals', - 'animal kingdom', - 'puppy', - 'doggy', - 'memes', - 'dogs', - 'perro', - 'puppies', - 'chien', - 'pugs', - 'puta', - 'pute' - ]), - Emoji( - name: 'cat face', - char: '\u{1F431}', - shortName: 'cat', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'cat', - 'face', - 'pet', - 'uc6', - 'animal', - 'halloween', - 'cat', - 'vagina', - 'pussy', - 'glitter', - 'pets', - 'pokemon', - 'porn', - 'animals', - 'animal kingdom', - 'samhain', - 'kitty', - 'kitten', - 'cats', - 'kittens', - 'kitties', - 'feline', - 'felines', - 'cat face', - 'gato', - 'meow', - 'condom' - ]), - Emoji( - name: 'mouse face', - char: '\u{1F42D}', - shortName: 'mouse', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'face', - 'mouse', - 'uc6', - 'animal', - 'mickey', - 'disney', - 'pokemon', - 'rodent', - 'animals', - 'animal kingdom', - 'cartoon' - ]), - Emoji( - name: 'hamster', - char: '\u{1F439}', - shortName: 'hamster', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'face', - 'hamster', - 'pet', - 'uc6', - 'animal', - 'pets', - 'rodent', - 'animals', - 'animal kingdom' - ]), - Emoji( - name: 'rabbit face', - char: '\u{1F430}', - shortName: 'rabbit', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'bunny', - 'face', - 'pet', - 'rabbit', - 'uc6', - 'animal', - 'wildlife', - 'magic', - 'easter', - 'pets', - 'pokemon', - 'playboy', - 'animals', - 'animal kingdom', - 'spell', - 'genie', - 'magical', - 'play boy' - ]), - Emoji( - name: 'fox', - char: '\u{1F98A}', - shortName: 'fox', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'face', - 'fox', - 'uc9', - 'animal', - 'wildlife', - 'forest', - 'animals', - 'animal kingdom', - 'rainforest' - ]), - Emoji( - name: 'bear', - char: '\u{1F43B}', - shortName: 'bear', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'bear', - 'face', - 'uc6', - 'animal', - 'wildlife', - 'roar', - 'gummy', - 'california', - 'forest', - 'animals', - 'animal kingdom', - 'rawr', - 'rainforest' - ]), - Emoji( - name: 'panda', - char: '\u{1F43C}', - shortName: 'panda_face', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'face', - 'panda', - 'uc6', - 'animal', - 'wildlife', - 'roar', - 'chinese', - 'forest', - 'animals', - 'animal kingdom', - 'rawr', - 'chinois', - 'asian', - 'chine', - 'rainforest' - ]), - Emoji( - name: 'polar bear', - char: '\u{1F43B}\u{200D}\u{2744}\u{FE0F}', - shortName: 'polar_bear', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: ['uc13', 'animal', 'polar bear', 'animals', 'animal kingdom']), - Emoji( - name: 'koala', - char: '\u{1F428}', - shortName: 'koala', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'bear', - 'uc6', - 'animal', - 'wildlife', - 'australia', - 'forest', - 'marsupial', - 'animals', - 'animal kingdom', - 'rainforest', - 'marsupials' - ]), - Emoji( - name: 'tiger face', - char: '\u{1F42F}', - shortName: 'tiger', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'face', - 'tiger', - 'uc6', - 'animal', - 'wildlife', - 'roar', - 'cat', - 'forest', - 'animals', - 'animal kingdom', - 'rawr', - 'kitty', - 'kitten', - 'cats', - 'kittens', - 'kitties', - 'feline', - 'felines', - 'cat face', - 'gato', - 'meow', - 'rainforest' - ]), - Emoji( - name: 'lion', - char: '\u{1F981}', - shortName: 'lion_face', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'Leo', - 'face', - 'lion', - 'zodiac', - 'uc8', - 'animal', - 'wildlife', - 'roar', - 'cat', - 'england', - 'forest', - 'animals', - 'animal kingdom', - 'rawr', - 'kitty', - 'kitten', - 'cats', - 'kittens', - 'kitties', - 'feline', - 'felines', - 'cat face', - 'gato', - 'meow', - 'united kingdom', - 'london', - 'uk', - 'rainforest' - ]), - Emoji( - name: 'cow face', - char: '\u{1F42E}', - shortName: 'cow', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'cow', - 'face', - 'uc6', - 'animal', - 'farm', - 'texas', - 'minecraft', - 'animals', - 'animal kingdom' - ]), - Emoji( - name: 'pig face', - char: '\u{1F437}', - shortName: 'pig', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'face', - 'pig', - 'uc6', - 'animal', - 'pig', - 'farm', - 'guinea pig', - 'pets', - 'minecraft', - 'animals', - 'animal kingdom', - 'pork' - ]), - Emoji( - name: 'pig nose', - char: '\u{1F43D}', - shortName: 'pig_nose', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'face', - 'nose', - 'pig', - 'uc6', - 'animal', - 'pig', - 'guinea pig', - 'pets', - 'animals', - 'animal kingdom', - 'pork' - ]), - Emoji( - name: 'frog', - char: '\u{1F438}', - shortName: 'frog', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalAmphibian, - keywords: [ - 'face', - 'frog', - 'uc6', - 'animal', - 'wildlife', - 'forest', - 'pets', - 'pokemon', - 'river', - 'animals', - 'animal kingdom', - 'rainforest' - ]), - Emoji( - name: 'monkey face', - char: '\u{1F435}', - shortName: 'monkey_face', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'face', - 'monkey', - 'uc6', - 'animal', - 'monkey', - 'animals', - 'animal kingdom', - 'progi', - 'ape', - 'primate' - ]), - Emoji( - name: 'see-no-evil monkey', - char: '\u{1F648}', - shortName: 'see_no_evil', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.monkeyFace, - keywords: [ - 'evil', - 'face', - 'forbidden', - 'gesture', - 'monkey', - 'no', - 'not', - 'prohibited', - 'see', - 'uc6', - 'animal', - 'monkey', - 'porn', - 'shame', - 'animals', - 'animal kingdom', - 'progi', - 'ape', - 'primate' - ]), - Emoji( - name: 'hear-no-evil monkey', - char: '\u{1F649}', - shortName: 'hear_no_evil', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.monkeyFace, - keywords: [ - 'evil', - 'face', - 'forbidden', - 'gesture', - 'hear', - 'monkey', - 'no', - 'not', - 'prohibited', - 'uc6', - 'animal', - 'monkey', - 'animals', - 'animal kingdom', - 'progi', - 'ape', - 'primate' - ]), - Emoji( - name: 'speak-no-evil monkey', - char: '\u{1F64A}', - shortName: 'speak_no_evil', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.monkeyFace, - keywords: [ - 'evil', - 'face', - 'forbidden', - 'gesture', - 'monkey', - 'no', - 'not', - 'prohibited', - 'speak', - 'uc6', - 'animal', - 'monkey', - 'facebook', - 'quiet', - 'animals', - 'animal kingdom', - 'progi', - 'ape', - 'primate', - 'shut up', - 'hushed', - 'silence', - 'silent', - 'shush', - 'shh' - ]), - Emoji( - name: 'monkey', - char: '\u{1F412}', - shortName: 'monkey', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'monkey', - 'uc6', - 'animal', - 'wildlife', - 'monkey', - 'forest', - 'pokemon', - 'animals', - 'animal kingdom', - 'progi', - 'ape', - 'primate', - 'rainforest' - ]), - Emoji( - name: 'chicken', - char: '\u{1F414}', - shortName: 'chicken', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBird, - keywords: [ - 'bird', - 'chicken', - 'uc6', - 'animal', - 'birds', - 'farm', - 'jewish', - 'pets', - 'minecraft', - 'animals', - 'animal kingdom', - 'goose', - 'hannukah', - 'hanukkah', - 'israel' - ]), - Emoji( - name: 'penguin', - char: '\u{1F427}', - shortName: 'penguin', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBird, - keywords: [ - 'bird', - 'penguin', - 'uc6', - 'animal', - 'wildlife', - 'birds', - 'ocean', - 'animals', - 'animal kingdom', - 'goose', - 'sea' - ]), - Emoji( - name: 'bird', - char: '\u{1F426}', - shortName: 'bird', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBird, - keywords: [ - 'bird', - 'uc6', - 'animal', - 'wildlife', - 'twitter', - 'birds', - 'forest', - 'pets', - 'pokemon', - 'animals', - 'animal kingdom', - 'goose', - 'rainforest' - ]), - Emoji( - name: 'baby chick', - char: '\u{1F424}', - shortName: 'baby_chick', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBird, - keywords: [ - 'baby', - 'bird', - 'chick', - 'uc6', - 'animal', - 'easter', - 'birds', - 'pets', - 'animals', - 'animal kingdom', - 'goose' - ]), - Emoji( - name: 'hatching chick', - char: '\u{1F423}', - shortName: 'hatching_chick', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBird, - keywords: [ - 'baby', - 'bird', - 'chick', - 'hatching', - 'uc6', - 'animal', - 'easter', - 'birds', - 'eggs', - 'pets', - 'animals', - 'animal kingdom', - 'goose' - ]), - Emoji( - name: 'front-facing baby chick', - char: '\u{1F425}', - shortName: 'hatched_chick', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBird, - keywords: [ - 'baby', - 'bird', - 'chick', - 'uc6', - 'animal', - 'easter', - 'birds', - 'pets', - 'pokemon', - 'minecraft', - 'animals', - 'animal kingdom', - 'goose' - ]), - Emoji( - name: 'duck', - char: '\u{1F986}', - shortName: 'duck', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBird, - keywords: [ - 'bird', - 'duck', - 'uc9', - 'animal', - 'wildlife', - 'quack', - 'birds', - 'hunt', - 'animals', - 'animal kingdom', - 'goose' - ]), - Emoji( - name: 'dodo', - char: '\u{1F9A4}', - shortName: 'dodo', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBird, - keywords: [ - 'uc13', - 'animal', - 'birds', - 'animals', - 'animal kingdom', - 'goose' - ]), - Emoji( - name: 'eagle', - char: '\u{1F985}', - shortName: 'eagle', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBird, - keywords: [ - 'bird', - 'eagle', - 'uc9', - 'animal', - 'wildlife', - 'america', - 'birds', - 'forest', - 'pokemon', - 'independence day', - 'animals', - 'animal kingdom', - 'usa', - 'united states', - 'united states of america', - 'american', - 'goose', - 'rainforest', - '4th of july' - ]), - Emoji( - name: 'owl', - char: '\u{1F989}', - shortName: 'owl', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBird, - keywords: [ - 'bird', - 'owl', - 'wise', - 'uc9', - 'animal', - 'wildlife', - 'halloween', - 'birds', - 'forest', - 'animals', - 'animal kingdom', - 'samhain', - 'goose', - 'rainforest' - ]), - Emoji( - name: 'bat', - char: '\u{1F987}', - shortName: 'bat', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'bat', - 'vampire', - 'uc9', - 'animal', - 'wildlife', - 'halloween', - 'harry potter', - 'forest', - 'pokemon', - 'super hero', - 'vampire', - 'animals', - 'animal kingdom', - 'samhain', - 'rainforest', - 'superhero', - 'superman', - 'batman', - 'dracula' - ]), - Emoji( - name: 'wolf', - char: '\u{1F43A}', - shortName: 'wolf', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'face', - 'wolf', - 'uc6', - 'animal', - 'wildlife', - 'roar', - 'forest', - 'pokemon', - 'minecraft', - 'animals', - 'animal kingdom', - 'rawr', - 'rainforest' - ]), - Emoji( - name: 'boar', - char: '\u{1F417}', - shortName: 'boar', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'pig', - 'uc6', - 'animal', - 'wildlife', - 'pig', - 'farm', - 'forest', - 'guinea pig', - 'hunt', - 'animals', - 'animal kingdom', - 'pork', - 'rainforest' - ]), - Emoji( - name: 'horse face', - char: '\u{1F434}', - shortName: 'horse', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'face', - 'horse', - 'uc6', - 'animal', - 'wildlife', - 'horse racing', - 'donkey', - 'farm', - 'pets', - 'pokemon', - 'texas', - 'minecraft', - 'horse', - 'animals', - 'animal kingdom', - 'horseback riding', - 'horse and rider', - 'horses', - 'horseshoe', - 'pony', - 'poney' - ]), - Emoji( - name: 'unicorn', - char: '\u{1F984}', - shortName: 'unicorn', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'face', - 'unicorn', - 'uc8', - 'animal', - 'halloween', - 'emojione', - 'hug', - 'lgbt', - 'unicorn', - 'pink', - 'facebook', - 'dream', - 'pokemon', - 'fantasy', - 'animals', - 'animal kingdom', - 'samhain', - 'emoji one', - 'embrace', - 'hugs', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'unicorns', - 'onesie', - 'rose', - 'dreams' - ]), - Emoji( - name: 'honeybee', - char: '\u{1F41D}', - shortName: 'bee', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBug, - keywords: [ - 'bee', - 'insect', - 'uc6', - 'animal', - 'wildlife', - 'insects', - 'animals', - 'animal kingdom', - 'insect', - 'bugs', - 'bug' - ]), - Emoji( - name: 'bug', - char: '\u{1F41B}', - shortName: 'bug', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBug, - keywords: [ - 'insect', - 'uc6', - 'animal', - 'wildlife', - 'insects', - 'gummy', - 'forest', - 'pokemon', - 'worm', - 'animals', - 'animal kingdom', - 'insect', - 'bugs', - 'bug', - 'rainforest', - 'caterpillar' - ]), - Emoji( - name: 'butterfly', - char: '\u{1F98B}', - shortName: 'butterfly', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBug, - keywords: [ - 'butterfly', - 'insect', - 'pretty', - 'uc9', - 'animal', - 'wildlife', - 'insects', - 'forest', - 'pokemon', - 'animals', - 'animal kingdom', - 'insect', - 'bugs', - 'bug', - 'rainforest' - ]), - Emoji( - name: 'snail', - char: '\u{1F40C}', - shortName: 'snail', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBug, - keywords: [ - 'snail', - 'uc6', - 'animal', - 'wildlife', - 'insects', - 'emojione', - 'animals', - 'animal kingdom', - 'insect', - 'bugs', - 'bug', - 'emoji one' - ]), - Emoji( - name: 'worm', - char: '\u{1FAB1}', - shortName: 'worm', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBug, - keywords: ['uc13', 'animal', 'wildlife', 'animals', 'animal kingdom']), - Emoji( - name: 'lady beetle', - char: '\u{1F41E}', - shortName: 'lady_beetle', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBug, - keywords: [ - 'beetle', - 'insect', - 'ladybird', - 'ladybug', - 'uc6', - 'animal', - 'wildlife', - 'insects', - 'animals', - 'animal kingdom', - 'insect', - 'bugs', - 'bug' - ]), - Emoji( - name: 'ant', - char: '\u{1F41C}', - shortName: 'ant', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBug, - keywords: [ - 'insect', - 'uc6', - 'animal', - 'wildlife', - 'insects', - 'pokemon', - 'animals', - 'animal kingdom', - 'insect', - 'bugs', - 'bug' - ]), - Emoji( - name: 'fly', - char: '\u{1FAB0}', - shortName: 'fly', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBug, - keywords: [ - 'uc13', - 'animal', - 'wildlife', - 'insects', - 'shit', - 'animals', - 'animal kingdom', - 'insect', - 'bugs', - 'bug', - 'poop', - 'turd', - 'feces', - 'pile', - 'merde', - 'butthole', - 'caca', - 'crap', - 'dirty', - 'pooo', - 'mess', - 'brown', - 'poopoo' - ]), - Emoji( - name: 'mosquito', - char: '\u{1F99F}', - shortName: 'mosquito', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBug, - keywords: [ - 'uc11', - 'animal', - 'wildlife', - 'insects', - 'bite', - 'animals', - 'animal kingdom', - 'insect', - 'bugs', - 'bug' - ]), - Emoji( - name: 'cockroach', - char: '\u{1FAB3}', - shortName: 'cockroach', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBug, - keywords: [ - 'uc13', - 'animal', - 'wildlife', - 'insects', - 'animals', - 'animal kingdom', - 'insect', - 'bugs', - 'bug' - ]), - Emoji( - name: 'beetle', - char: '\u{1FAB2}', - shortName: 'beetle', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBug, - keywords: [ - 'uc13', - 'animal', - 'wildlife', - 'insects', - 'animals', - 'animal kingdom', - 'insect', - 'bugs', - 'bug' - ]), - Emoji( - name: 'cricket', - char: '\u{1F997}', - shortName: 'cricket', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBug, - keywords: [ - 'uc10', - 'animal', - 'wildlife', - 'insects', - 'animals', - 'animal kingdom', - 'insect', - 'bugs', - 'bug' - ]), - Emoji( - name: 'spider', - char: '\u{1F577}\u{FE0F}', - shortName: 'spider', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBug, - keywords: [ - 'insect', - 'uc7', - 'animal', - 'wildlife', - 'insects', - 'halloween', - 'australia', - 'harry potter', - 'forest', - 'pets', - 'animals', - 'animal kingdom', - 'insect', - 'bugs', - 'bug', - 'samhain', - 'rainforest' - ]), - Emoji( - name: 'spider web', - char: '\u{1F578}\u{FE0F}', - shortName: 'spider_web', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBug, - keywords: [ - 'spider', - 'web', - 'uc7', - 'halloween', - 'forest', - 'samhain', - 'rainforest' - ]), - Emoji( - name: 'scorpion', - char: '\u{1F982}', - shortName: 'scorpion', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBug, - keywords: [ - 'Scorpius', - 'scorpio', - 'zodiac', - 'uc8', - 'animal', - 'wildlife', - 'insects', - 'reptile', - 'animals', - 'animal kingdom', - 'insect', - 'bugs', - 'bug', - 'reptiles' - ]), - Emoji( - name: 'turtle', - char: '\u{1F422}', - shortName: 'turtle', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalReptile, - keywords: [ - 'terrapin', - 'tortoise', - 'turtle', - 'uc6', - 'animal', - 'wildlife', - 'reptile', - 'pets', - 'pokemon', - 'river', - 'ocean', - 'animals', - 'animal kingdom', - 'reptiles', - 'sea' - ]), - Emoji( - name: 'snake', - char: '\u{1F40D}', - shortName: 'snake', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalReptile, - keywords: [ - 'Ophiuchus', - 'bearer', - 'serpent', - 'zodiac', - 'uc6', - 'animal', - 'wildlife', - 'reptile', - 'creationism', - 'australia', - 'harry potter', - 'forest', - 'pets', - 'pokemon', - 'ocean', - 'animals', - 'animal kingdom', - 'reptiles', - 'adam & eve', - 'adam and eve', - 'rainforest', - 'sea' - ]), - Emoji( - name: 'lizard', - char: '\u{1F98E}', - shortName: 'lizard', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalReptile, - keywords: [ - 'lizard', - 'reptile', - 'uc9', - 'animal', - 'wildlife', - 'reptile', - 'forest', - 'pets', - 'pokemon', - 'animals', - 'animal kingdom', - 'reptiles', - 'rainforest' - ]), - Emoji( - name: 'T-Rex', - char: '\u{1F996}', - shortName: 't_rex', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalReptile, - keywords: [ - 'Tyrannosaurus Rex', - 'uc10', - 'animal', - 'dinosaur', - 'Tyrannosaurus Rex', - 'animals', - 'animal kingdom', - 'trex', - 't rex' - ]), - Emoji( - name: 'sauropod', - char: '\u{1F995}', - shortName: 'sauropod', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalReptile, - keywords: [ - 'brachiosaurus', - 'brontosaurus', - 'diplodocus', - 'uc10', - 'animal', - 'dinosaur', - 'Brontosaurus', - 'animals', - 'animal kingdom', - 'Diplodocus', - 'Brachiosaurus' - ]), - Emoji( - name: 'octopus', - char: '\u{1F419}', - shortName: 'octopus', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMarine, - keywords: [ - 'octopus', - 'uc6', - 'animal', - 'wildlife', - 'pussy', - 'pokemon', - 'porn', - 'scuba', - 'seafood', - 'ocean', - 'animals', - 'animal kingdom', - 'condom', - 'snorkel', - 'sea' - ]), - Emoji( - name: 'squid', - char: '\u{1F991}', - shortName: 'squid', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodMarine, - keywords: [ - 'food', - 'molusc', - 'squid', - 'uc9', - 'animal', - 'wildlife', - 'scuba', - 'seafood', - 'ocean', - 'animals', - 'animal kingdom', - 'snorkel', - 'sea' - ]), - Emoji( - name: 'shrimp', - char: '\u{1F990}', - shortName: 'shrimp', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodMarine, - keywords: [ - 'food', - 'shellfish', - 'shrimp', - 'small', - 'uc9', - 'animal', - 'wildlife', - 'prawn', - 'scuba', - 'seafood', - 'ocean', - 'crustacean', - 'animals', - 'animal kingdom', - 'snorkel', - 'sea' - ]), - Emoji( - name: 'lobster', - char: '\u{1F99E}', - shortName: 'lobster', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodMarine, - keywords: [ - 'uc11', - 'animal', - 'food', - 'wildlife', - 'seafood', - 'ocean', - 'crustacean', - 'animals', - 'animal kingdom', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'sea' - ]), - Emoji( - name: 'crab', - char: '\u{1F980}', - shortName: 'crab', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodMarine, - keywords: [ - 'Cancer', - 'zodiac', - 'uc8', - 'animal', - 'wildlife', - 'tropical', - 'pokemon', - 'scuba', - 'seafood', - 'ocean', - 'crustacean', - 'animals', - 'animal kingdom', - 'snorkel', - 'sea' - ]), - Emoji( - name: 'blowfish', - char: '\u{1F421}', - shortName: 'blowfish', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMarine, - keywords: [ - 'fish', - 'uc6', - 'animal', - 'wildlife', - 'japan', - 'scuba', - 'ocean', - 'animals', - 'animal kingdom', - 'japanese', - 'ninja', - 'snorkel', - 'sea' - ]), - Emoji( - name: 'tropical fish', - char: '\u{1F420}', - shortName: 'tropical_fish', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMarine, - keywords: [ - 'fish', - 'tropical', - 'uc6', - 'animal', - 'wildlife', - 'tropical', - 'pets', - 'scuba', - 'ocean', - 'animals', - 'animal kingdom', - 'snorkel', - 'sea' - ]), - Emoji( - name: 'fish', - char: '\u{1F41F}', - shortName: 'fish', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMarine, - keywords: [ - 'Pisces', - 'zodiac', - 'uc6', - 'animal', - 'wildlife', - 'tropical', - 'pets', - 'scuba', - 'seafood', - 'river', - 'ocean', - 'animals', - 'animal kingdom', - 'snorkel', - 'sea' - ]), - Emoji( - name: 'seal', - char: '\u{1F9AD}', - shortName: 'seal', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMarine, - keywords: [ - 'uc13', - 'animal', - 'wildlife', - 'ocean', - 'animals', - 'animal kingdom', - 'sea' - ]), - Emoji( - name: 'dolphin', - char: '\u{1F42C}', - shortName: 'dolphin', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMarine, - keywords: [ - 'flipper', - 'uc6', - 'animal', - 'wildlife', - 'tropical', - 'florida', - 'scuba', - 'ocean', - 'animals', - 'animal kingdom', - 'snorkel', - 'sea' - ]), - Emoji( - name: 'spouting whale', - char: '\u{1F433}', - shortName: 'whale', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMarine, - keywords: [ - 'face', - 'spouting', - 'whale', - 'uc6', - 'animal', - 'wildlife', - 'tropical', - 'whales', - 'scuba', - 'ocean', - 'animals', - 'animal kingdom', - 'whale', - 'moby', - 'snorkel', - 'sea' - ]), - Emoji( - name: 'whale', - char: '\u{1F40B}', - shortName: 'whale2', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMarine, - keywords: [ - 'whale', - 'uc6', - 'animal', - 'wildlife', - 'tropical', - 'whales', - 'scuba', - 'ocean', - 'animals', - 'animal kingdom', - 'whale', - 'moby', - 'snorkel', - 'sea' - ]), - Emoji( - name: 'shark', - char: '\u{1F988}', - shortName: 'shark', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMarine, - keywords: [ - 'fish', - 'shark', - 'uc9', - 'animal', - 'wildlife', - 'florida', - 'scuba', - 'ocean', - 'animals', - 'animal kingdom', - 'snorkel', - 'sea' - ]), - Emoji( - name: 'crocodile', - char: '\u{1F40A}', - shortName: 'crocodile', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalReptile, - keywords: [ - 'crocodile', - 'uc6', - 'animal', - 'wildlife', - 'reptile', - 'florida', - 'river', - 'alligator', - 'animals', - 'animal kingdom', - 'reptiles' - ]), - Emoji( - name: 'tiger', - char: '\u{1F405}', - shortName: 'tiger2', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'tiger', - 'uc6', - 'animal', - 'wildlife', - 'roar', - 'cat', - 'circus', - 'forest', - 'pokemon', - 'animals', - 'animal kingdom', - 'rawr', - 'kitty', - 'kitten', - 'cats', - 'kittens', - 'kitties', - 'feline', - 'felines', - 'cat face', - 'gato', - 'meow', - 'circus tent', - 'clown', - 'clowns', - 'rainforest' - ]), - Emoji( - name: 'leopard', - char: '\u{1F406}', - shortName: 'leopard', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'leopard', - 'uc6', - 'animal', - 'wildlife', - 'roar', - 'cat', - 'forest', - 'animals', - 'animal kingdom', - 'rawr', - 'kitty', - 'kitten', - 'cats', - 'kittens', - 'kitties', - 'feline', - 'felines', - 'cat face', - 'gato', - 'meow', - 'rainforest' - ]), - Emoji( - name: 'zebra', - char: '\u{1F993}', - shortName: 'zebra', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'stripe', - 'uc10', - 'animal', - 'wildlife', - 'horse', - 'animals', - 'animal kingdom' - ]), - Emoji( - name: 'gorilla', - char: '\u{1F98D}', - shortName: 'gorilla', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'gorilla', - 'uc9', - 'animal', - 'wildlife', - 'forest', - 'pokemon', - 'animals', - 'animal kingdom', - 'rainforest' - ]), - Emoji( - name: 'orangutan', - char: '\u{1F9A7}', - shortName: 'orangutan', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'uc12', - 'animal', - 'wildlife', - 'monkey', - 'animals', - 'animal kingdom', - 'progi', - 'ape', - 'primate' - ]), - Emoji( - name: 'elephant', - char: '\u{1F418}', - shortName: 'elephant', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'elephant', - 'uc6', - 'animal', - 'wildlife', - 'circus', - 'elephant', - 'animals', - 'animal kingdom', - 'circus tent', - 'clown', - 'clowns' - ]), - Emoji( - name: 'mammoth', - char: '\u{1F9A3}', - shortName: 'mammoth', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'uc13', - 'animal', - 'wildlife', - 'mammuthus', - 'elephant', - 'animals', - 'animal kingdom' - ]), - Emoji( - name: 'bison', - char: '\u{1F9AC}', - shortName: 'bison', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: ['uc13', 'animal', 'wildlife', 'animals', 'animal kingdom']), - Emoji( - name: 'hippopotamus', - char: '\u{1F99B}', - shortName: 'hippopotamus', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: ['uc11', 'animal', 'wildlife', 'animals', 'animal kingdom']), - Emoji( - name: 'rhinoceros', - char: '\u{1F98F}', - shortName: 'rhino', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'rhinoceros', - 'uc9', - 'animal', - 'wildlife', - 'forest', - 'pokemon', - 'animals', - 'animal kingdom', - 'rainforest' - ]), - Emoji( - name: 'camel', - char: '\u{1F42A}', - shortName: 'dromedary_camel', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'dromedary', - 'hump', - 'uc6', - 'animal', - 'wildlife', - 'animals', - 'animal kingdom' - ]), - Emoji( - name: 'two-hump camel', - char: '\u{1F42B}', - shortName: 'camel', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'bactrian', - 'camel', - 'hump', - 'uc6', - 'animal', - 'wildlife', - 'hump day', - 'animals', - 'animal kingdom' - ]), - Emoji( - name: 'giraffe', - char: '\u{1F992}', - shortName: 'giraffe', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'spots', - 'uc10', - 'animal', - 'wildlife', - 'animals', - 'animal kingdom' - ]), - Emoji( - name: 'kangaroo', - char: '\u{1F998}', - shortName: 'kangaroo', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'uc11', - 'animal', - 'wildlife', - 'australia', - 'marsupial', - 'animals', - 'animal kingdom', - 'marsupials' - ]), - Emoji( - name: 'water buffalo', - char: '\u{1F403}', - shortName: 'water_buffalo', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'buffalo', - 'water', - 'uc6', - 'animal', - 'wildlife', - 'scotland', - 'animals', - 'animal kingdom', - 'scottish' - ]), - Emoji( - name: 'ox', - char: '\u{1F402}', - shortName: 'ox', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'Taurus', - 'bull', - 'zodiac', - 'uc6', - 'animal', - 'farm', - 'texas', - 'animals', - 'animal kingdom' - ]), - Emoji( - name: 'cow', - char: '\u{1F404}', - shortName: 'cow2', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'cow', - 'uc6', - 'animal', - 'farm', - 'texas', - 'animals', - 'animal kingdom' - ]), - Emoji( - name: 'horse', - char: '\u{1F40E}', - shortName: 'racehorse', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'equestrian', - 'racehorse', - 'racing', - 'uc6', - 'animal', - 'wildlife', - 'horse racing', - 'donkey', - 'farm', - 'pets', - 'pokemon', - 'texas', - 'viking', - 'horse', - 'animals', - 'animal kingdom', - 'horseback riding', - 'horse and rider', - 'horses', - 'horseshoe', - 'pony', - 'poney', - 'knight' - ]), - Emoji( - name: 'pig', - char: '\u{1F416}', - shortName: 'pig2', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'sow', - 'uc6', - 'animal', - 'pink', - 'pig', - 'farm', - 'guinea pig', - 'pets', - 'minecraft', - 'animals', - 'animal kingdom', - 'rose', - 'pork' - ]), - Emoji( - name: 'ram', - char: '\u{1F40F}', - shortName: 'ram', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'Aries', - 'male', - 'sheep', - 'zodiac', - 'uc6', - 'animal', - 'wildlife', - 'farm', - 'sheep', - 'animals', - 'animal kingdom' - ]), - Emoji( - name: 'ewe', - char: '\u{1F411}', - shortName: 'sheep', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'female', - 'sheep', - 'uc6', - 'animal', - 'farm', - 'lamb', - 'sheep', - 'animals', - 'animal kingdom' - ]), - Emoji( - name: 'llama', - char: '\u{1F999}', - shortName: 'llama', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: ['uc11', 'animal', 'farm', 'animals', 'animal kingdom']), - Emoji( - name: 'goat', - char: '\u{1F410}', - shortName: 'goat', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'Capricorn', - 'zodiac', - 'uc6', - 'animal', - 'farm', - 'animals', - 'animal kingdom' - ]), - Emoji( - name: 'deer', - char: '\u{1F98C}', - shortName: 'deer', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'deer', - 'uc9', - 'animal', - 'wildlife', - 'christmas', - 'raindeer', - 'forest', - 'hunt', - 'animals', - 'animal kingdom', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'moose', - 'rudolph', - 'rainforest' - ]), - Emoji( - name: 'dog', - char: '\u{1F415}', - shortName: 'dog2', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'pet', - 'uc6', - 'animal', - 'dog', - 'japan', - 'pug', - 'bingo', - 'bitch', - 'farm', - 'pets', - 'pokemon', - 'minecraft', - 'hunt', - 'animals', - 'animal kingdom', - 'puppy', - 'doggy', - 'memes', - 'dogs', - 'perro', - 'puppies', - 'chien', - 'japanese', - 'ninja', - 'pugs', - 'puta', - 'pute' - ]), - Emoji( - name: 'poodle', - char: '\u{1F429}', - shortName: 'poodle', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'dog', - 'uc6', - 'animal', - 'dog', - 'pink', - 'paris', - 'bitch', - 'pets', - 'animals', - 'animal kingdom', - 'puppy', - 'doggy', - 'memes', - 'dogs', - 'perro', - 'puppies', - 'chien', - 'rose', - 'french', - 'france', - 'puta', - 'pute' - ]), - Emoji( - name: 'guide dog', - char: '\u{1F9AE}', - shortName: 'guide_dog', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'uc12', - 'animal', - 'dog', - 'handicap', - 'pets', - 'blind', - 'animals', - 'animal kingdom', - 'puppy', - 'doggy', - 'memes', - 'dogs', - 'perro', - 'puppies', - 'chien', - 'disabled', - 'disability' - ]), - Emoji( - name: 'service dog', - char: '\u{1F415}\u{200D}\u{1F9BA}', - shortName: 'service_dog', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'uc12', - 'animal', - 'dog', - 'handicap', - 'pets', - 'blind', - 'animals', - 'animal kingdom', - 'puppy', - 'doggy', - 'memes', - 'dogs', - 'perro', - 'puppies', - 'chien', - 'disabled', - 'disability' - ]), - Emoji( - name: 'cat', - char: '\u{1F408}', - shortName: 'cat2', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'pet', - 'uc6', - 'animal', - 'cat', - 'pussy', - 'grass', - 'farm', - 'pets', - 'pokemon', - 'porn', - 'animals', - 'animal kingdom', - 'kitty', - 'kitten', - 'cats', - 'kittens', - 'kitties', - 'feline', - 'felines', - 'cat face', - 'gato', - 'meow', - 'condom' - ]), - Emoji( - name: 'black cat', - char: '\u{1F408}\u{200D}\u{2B1B}', - shortName: 'black_cat', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'uc13', - 'animal', - 'halloween', - 'cat', - 'luck', - 'sol', - 'farm', - 'animals', - 'animal kingdom', - 'samhain', - 'kitty', - 'kitten', - 'cats', - 'kittens', - 'kitties', - 'feline', - 'felines', - 'cat face', - 'gato', - 'meow', - 'good luck', - 'lucky', - 'shit outta luck', - 'shit out of luck', - 'bad luck' - ]), - Emoji( - name: 'rooster', - char: '\u{1F413}', - shortName: 'rooster', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBird, - keywords: [ - 'bird', - 'rooster', - 'uc6', - 'animal', - 'wildlife', - 'birds', - 'farm', - 'animals', - 'animal kingdom', - 'goose' - ]), - Emoji( - name: 'turkey', - char: '\u{1F983}', - shortName: 'turkey', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBird, - keywords: [ - 'bird', - 'turkey', - 'uc8', - 'animal', - 'wildlife', - 'birds', - 'farm', - 'thanksgiving', - 'animals', - 'animal kingdom', - 'goose' - ]), - Emoji( - name: 'peacock', - char: '\u{1F99A}', - shortName: 'peacock', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBird, - keywords: [ - 'uc11', - 'animal', - 'wildlife', - 'birds', - 'farm', - 'animals', - 'animal kingdom', - 'goose' - ]), - Emoji( - name: 'parrot', - char: '\u{1F99C}', - shortName: 'parrot', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBird, - keywords: [ - 'uc11', - 'animal', - 'wildlife', - 'tropical', - 'pirate', - 'birds', - 'animals', - 'animal kingdom', - 'goose' - ]), - Emoji( - name: 'swan', - char: '\u{1F9A2}', - shortName: 'swan', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBird, - keywords: [ - 'uc11', - 'animal', - 'wildlife', - 'birds', - 'animals', - 'animal kingdom', - 'goose' - ]), - Emoji( - name: 'flamingo', - char: '\u{1F9A9}', - shortName: 'flamingo', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBird, - keywords: [ - 'uc12', - 'animal', - 'wildlife', - 'pink', - 'birds', - 'animals', - 'animal kingdom', - 'rose', - 'goose' - ]), - Emoji( - name: 'dove', - char: '\u{1F54A}\u{FE0F}', - shortName: 'dove', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBird, - keywords: [ - 'bird', - 'fly', - 'peace', - 'uc7', - 'animal', - 'wildlife', - 'wedding', - 'religion', - 'peace', - 'pray', - 'birds', - 'animals', - 'animal kingdom', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'peace out', - 'peace sign', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'goose' - ]), - Emoji( - name: 'rabbit', - char: '\u{1F407}', - shortName: 'rabbit2', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'bunny', - 'pet', - 'uc6', - 'animal', - 'wildlife', - 'magic', - 'easter', - 'pets', - 'pokemon', - 'hunt', - 'animals', - 'animal kingdom', - 'spell', - 'genie', - 'magical' - ]), - Emoji( - name: 'raccoon', - char: '\u{1F99D}', - shortName: 'raccoon', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'uc11', - 'animal', - 'wildlife', - 'forest', - 'animals', - 'animal kingdom', - 'rainforest' - ]), - Emoji( - name: 'skunk', - char: '\u{1F9A8}', - shortName: 'skunk', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'uc12', - 'animal', - 'wildlife', - 'stinky', - 'forest', - 'animals', - 'animal kingdom', - 'smell', - 'stink', - 'odor', - 'rainforest' - ]), - Emoji( - name: 'badger', - char: '\u{1F9A1}', - shortName: 'badger', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'uc11', - 'animal', - 'wildlife', - 'forest', - 'animals', - 'animal kingdom', - 'rainforest' - ]), - Emoji( - name: 'beaver', - char: '\u{1F9AB}', - shortName: 'beaver', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'uc13', - 'animal', - 'wildlife', - 'forest', - 'rodent', - 'beaver', - 'animals', - 'animal kingdom', - 'rainforest' - ]), - Emoji( - name: 'otter', - char: '\u{1F9A6}', - shortName: 'otter', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'uc12', - 'animal', - 'wildlife', - 'forest', - 'ocean', - 'otor', - 'animals', - 'animal kingdom', - 'rainforest', - 'sea', - 'oter', - 'wódr̥' - ]), - Emoji( - name: 'sloth', - char: '\u{1F9A5}', - shortName: 'sloth', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'uc12', - 'animal', - 'wildlife', - 'forest', - 'lazy', - 'animals', - 'animal kingdom', - 'rainforest' - ]), - Emoji( - name: 'mouse', - char: '\u{1F401}', - shortName: 'mouse2', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'mouse', - 'uc6', - 'animal', - 'wildlife', - 'mickey', - 'disney', - 'pokemon', - 'rodent', - 'animals', - 'animal kingdom', - 'cartoon' - ]), - Emoji( - name: 'rat', - char: '\u{1F400}', - shortName: 'rat', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'rat', - 'uc6', - 'animal', - 'wildlife', - 'harry potter', - 'pokemon', - 'rodent', - 'animals', - 'animal kingdom' - ]), - Emoji( - name: 'chipmunk', - char: '\u{1F43F}\u{FE0F}', - shortName: 'chipmunk', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'chipmunk', - 'uc7', - 'animal', - 'wildlife', - 'squirrel', - 'forest', - 'pokemon', - 'rodent', - 'animals', - 'animal kingdom', - 'rainforest' - ]), - Emoji( - name: 'hedgehog', - char: '\u{1F994}', - shortName: 'hedgehog', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'spiny', - 'uc10', - 'animal', - 'wildlife', - 'erinaceinae', - 'animals', - 'animal kingdom' - ]), - Emoji( - name: 'paw prints', - char: '\u{1F43E}', - shortName: 'feet', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMammal, - keywords: [ - 'feet', - 'paw', - 'print', - 'uc6', - 'animal', - 'paws', - 'animals', - 'animal kingdom' - ]), - Emoji( - name: 'dragon', - char: '\u{1F409}', - shortName: 'dragon', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalReptile, - keywords: [ - 'fairy tale', - 'uc6', - 'animal', - 'roar', - 'reptile', - 'harry potter', - 'pokemon', - 'minecraft', - 'fantasy', - 'animals', - 'animal kingdom', - 'rawr', - 'reptiles' - ]), - Emoji( - name: 'dragon face', - char: '\u{1F432}', - shortName: 'dragon_face', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalReptile, - keywords: [ - 'dragon', - 'face', - 'fairy tale', - 'uc6', - 'animal', - 'roar', - 'monster', - 'reptile', - 'pokemon', - 'minecraft', - 'fantasy', - 'animals', - 'animal kingdom', - 'rawr', - 'monsters', - 'beast', - 'reptiles' - ]), - Emoji( - name: 'cactus', - char: '\u{1F335}', - shortName: 'cactus', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.plantOther, - keywords: [ - 'plant', - 'uc6', - 'nature', - 'plant', - 'trees', - 'plants', - 'tree', - 'branch', - 'wood' - ]), - Emoji( - name: 'Christmas tree', - char: '\u{1F384}', - shortName: 'christmas_tree', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.event, - keywords: [ - 'Christmas', - 'celebration', - 'tree', - 'uc6', - 'holidays', - 'plant', - 'christmas', - 'santa', - 'trees', - 'advent', - 'holiday', - 'plants', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'santa clause', - 'santa claus', - 'tree', - 'branch', - 'wood' - ]), - Emoji( - name: 'evergreen tree', - char: '\u{1F332}', - shortName: 'evergreen_tree', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.plantOther, - keywords: [ - 'tree', - 'uc6', - 'nature', - 'plant', - 'camp', - 'trees', - 'forest', - 'parks', - 'plants', - 'camping', - 'tent', - 'camper', - 'outdoor', - 'outside', - 'tree', - 'branch', - 'wood', - 'rainforest', - 'regional park', - 'nature park', - 'natural park' - ]), - Emoji( - name: 'deciduous tree', - char: '\u{1F333}', - shortName: 'deciduous_tree', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.plantOther, - keywords: [ - 'deciduous', - 'shedding', - 'tree', - 'uc6', - 'nature', - 'plant', - 'camp', - 'trees', - 'farm', - 'forest', - 'parks', - 'plants', - 'camping', - 'tent', - 'camper', - 'outdoor', - 'outside', - 'tree', - 'branch', - 'wood', - 'rainforest', - 'regional park', - 'nature park', - 'natural park' - ]), - Emoji( - name: 'palm tree', - char: '\u{1F334}', - shortName: 'palm_tree', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.plantOther, - keywords: [ - 'palm', - 'tree', - 'uc6', - 'nature', - 'plant', - 'tropical', - 'trees', - 'california', - 'coconut', - 'florida', - 'palma', - 'plants', - 'tree', - 'branch', - 'wood', - 'palmas' - ]), - Emoji( - name: 'seedling', - char: '\u{1F331}', - shortName: 'seedling', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.plantOther, - keywords: [ - 'young', - 'uc6', - 'nature', - 'plant', - 'trees', - 'leaf', - 'grass', - 'weed', - 'farm', - 'irish', - 'pokemon', - 'plants', - 'tree', - 'branch', - 'wood', - 'leaves', - 'saint patricks day', - 'st patricks day', - 'leprechaun' - ]), - Emoji( - name: 'herb', - char: '\u{1F33F}', - shortName: 'herb', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.plantOther, - keywords: [ - 'leaf', - 'uc6', - 'nature', - 'plant', - 'leaf', - 'grass', - 'weed', - 'farm', - 'plants', - 'leaves' - ]), - Emoji( - name: 'shamrock', - char: '\u{2618}\u{FE0F}', - shortName: 'shamrock', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.plantOther, - keywords: [ - 'plant', - 'uc4', - 'nature', - 'plant', - 'luck', - 'leaf', - 'grass', - 'irish', - 'plants', - 'good luck', - 'lucky', - 'leaves', - 'saint patricks day', - 'st patricks day', - 'leprechaun' - ]), - Emoji( - name: 'four leaf clover', - char: '\u{1F340}', - shortName: 'four_leaf_clover', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.plantOther, - keywords: [ - '4', - 'clover', - 'four', - 'leaf', - 'uc6', - 'nature', - 'plant', - 'luck', - 'leaf', - 'sol', - 'grass', - 'bingo', - 'irish', - 'plants', - 'good luck', - 'lucky', - 'leaves', - 'shit outta luck', - 'shit out of luck', - 'bad luck', - 'saint patricks day', - 'st patricks day', - 'leprechaun' - ]), - Emoji( - name: 'pine decoration', - char: '\u{1F38D}', - shortName: 'bamboo', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.event, - keywords: [ - 'Japanese', - 'bamboo', - 'celebration', - 'pine', - 'uc6', - 'nature', - 'plant', - 'japan', - 'plants', - 'japanese', - 'ninja' - ]), - Emoji( - name: 'tanabata tree', - char: '\u{1F38B}', - shortName: 'tanabata_tree', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.event, - keywords: [ - 'Japanese', - 'banner', - 'celebration', - 'tree', - 'uc6', - 'nature', - 'plant', - 'japan', - 'trees', - 'plants', - 'japanese', - 'ninja', - 'tree', - 'branch', - 'wood' - ]), - Emoji( - name: 'leaf fluttering in wind', - char: '\u{1F343}', - shortName: 'leaves', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.plantOther, - keywords: [ - 'blow', - 'flutter', - 'leaf', - 'wind', - 'uc6', - 'weather', - 'nature', - 'plant', - 'trees', - 'leaf', - 'storm', - 'autumn', - 'plants', - 'tree', - 'branch', - 'wood', - 'leaves', - 'fall' - ]), - Emoji( - name: 'fallen leaf', - char: '\u{1F342}', - shortName: 'fallen_leaf', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.plantOther, - keywords: [ - 'falling', - 'leaf', - 'uc6', - 'nature', - 'plant', - 'trees', - 'leaf', - 'autumn', - 'plants', - 'tree', - 'branch', - 'wood', - 'leaves', - 'fall' - ]), - Emoji( - name: 'maple leaf', - char: '\u{1F341}', - shortName: 'maple_leaf', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.plantOther, - keywords: [ - 'falling', - 'leaf', - 'maple', - 'uc6', - 'nature', - 'plant', - 'trees', - 'leaf', - 'autumn', - 'marijuana', - 'plants', - 'tree', - 'branch', - 'wood', - 'leaves', - 'fall' - ]), - Emoji( - name: 'feather', - char: '\u{1FAB6}', - shortName: 'feather', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBird, - keywords: [ - 'uc13', - 'animal', - 'nature', - 'lgbt', - 'birds', - 'plume', - 'animals', - 'animal kingdom', - 'homosexual', - 'bisex', - 'transgender', - 'non binary', - 'pansexuality', - 'intersex', - 'goose', - 'quill', - 'plumage', - 'feathering', - 'pluma', - 'piuma', - 'feder' - ]), - Emoji( - name: 'mushroom', - char: '\u{1F344}', - shortName: 'mushroom', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodVegetable, - keywords: [ - 'toadstool', - 'uc6', - 'food', - 'nature', - 'vegetables', - 'drugs', - 'plant', - 'disney', - 'poison', - 'pokemon', - 'mushroom', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'vegetable', - 'veggie', - 'legume', - 'drug', - 'narcotics', - 'plants', - 'cartoon', - 'toxic', - 'toxins' - ]), - Emoji( - name: 'spiral shell', - char: '\u{1F41A}', - shortName: 'shell', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalMarine, - keywords: [ - 'shell', - 'spiral', - 'uc6', - 'nature', - 'tropical', - 'scuba', - 'ocean', - 'snorkel', - 'sea' - ]), - Emoji( - name: 'rock', - char: '\u{1FAA8}', - shortName: 'rock', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'uc13', - 'nature', - 'parks', - 'climb', - 'boulder', - 'regional park', - 'nature park', - 'natural park', - 'pebble' - ]), - Emoji( - name: 'wood', - char: '\u{1FAB5}', - shortName: 'wood', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'uc13', - 'nature', - 'beaver', - 'parks', - 'regional park', - 'nature park', - 'natural park' - ]), - Emoji( - name: 'sheaf of rice', - char: '\u{1F33E}', - shortName: 'ear_of_rice', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.plantOther, - keywords: [ - 'ear', - 'grain', - 'rice', - 'uc6', - 'nature', - 'plant', - 'leaf', - 'grass', - 'farm', - 'plants', - 'leaves' - ]), - Emoji( - name: 'potted plant', - char: '\u{1FAB4}', - shortName: 'potted_plant', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.plantOther, - keywords: ['uc13', 'nature', 'plant', 'leaf', 'plants', 'leaves']), - Emoji( - name: 'bouquet', - char: '\u{1F490}', - shortName: 'bouquet', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.plantFlower, - keywords: [ - 'flower', - 'uc6', - 'nature', - 'wedding', - 'flower', - 'plant', - 'love', - 'rip', - 'condolence', - 'beautiful', - 'roses', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'flowers', - 'plants', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'rest in peace', - 'compassion', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely' - ]), - Emoji( - name: 'tulip', - char: '\u{1F337}', - shortName: 'tulip', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.plantFlower, - keywords: [ - 'flower', - 'uc6', - 'nature', - 'flower', - 'plant', - 'vagina', - 'beautiful', - 'girls night', - 'pink', - 'easter', - 'flowers', - 'plants', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'ladies night', - 'girls only', - 'girlfriend', - 'rose' - ]), - Emoji( - name: 'rose', - char: '\u{1F339}', - shortName: 'rose', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.plantFlower, - keywords: [ - 'flower', - 'uc6', - 'nature', - 'flower', - 'plant', - 'love', - 'rip', - 'condolence', - 'beautiful', - 'flowers', - 'plants', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'rest in peace', - 'compassion', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely' - ]), - Emoji( - name: 'wilted flower', - char: '\u{1F940}', - shortName: 'wilted_rose', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.plantFlower, - keywords: [ - 'flower', - 'wilted', - 'uc9', - 'flower', - 'halloween', - 'plant', - 'dead', - 'flowers', - 'samhain', - 'plants', - 'death', - 'die', - 'dying', - 'fart', - 'goth', - 'grave', - 'headstone', - 'horror', - 'hurt', - 'kill', - 'murder', - 'tomb', - 'toot', - 'died' - ]), - Emoji( - name: 'hibiscus', - char: '\u{1F33A}', - shortName: 'hibiscus', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.plantFlower, - keywords: [ - 'flower', - 'uc6', - 'nature', - 'flower', - 'plant', - 'tropical', - 'beautiful', - 'pink', - 'flowers', - 'plants', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'rose' - ]), - Emoji( - name: 'cherry blossom', - char: '\u{1F338}', - shortName: 'cherry_blossom', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.plantFlower, - keywords: [ - 'blossom', - 'cherry', - 'flower', - 'uc6', - 'nature', - 'flower', - 'plant', - 'japan', - 'tropical', - 'beautiful', - 'hawaii', - 'pink', - 'sakura', - 'flowers', - 'plants', - 'japanese', - 'ninja', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'rose' - ]), - Emoji( - name: 'blossom', - char: '\u{1F33C}', - shortName: 'blossom', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.plantFlower, - keywords: [ - 'flower', - 'uc6', - 'nature', - 'flower', - 'plant', - 'vagina', - 'beautiful', - 'flowers', - 'plants', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely' - ]), - Emoji( - name: 'sunflower', - char: '\u{1F33B}', - shortName: 'sunflower', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.plantFlower, - keywords: [ - 'flower', - 'sun', - 'uc6', - 'nature', - 'flower', - 'plant', - 'beautiful', - 'farm', - 'flowers', - 'plants', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely' - ]), - Emoji( - name: 'sun with face', - char: '\u{1F31E}', - shortName: 'sun_with_face', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'bright', - 'face', - 'sun', - 'uc6', - 'sun', - 'sky', - 'day', - 'hump day', - 'morning', - 'sunglasses', - 'california', - 'pokemon', - 'las vegas', - 'summer', - 'sunshine', - 'sunny', - 'eclipse', - 'solar', - 'solareclipse', - 'shiny', - 'good morning', - 'shades', - 'lunettes de soleil', - 'sun glasses', - 'vegas', - 'weekend' - ]), - Emoji( - name: 'full moon face', - char: '\u{1F31D}', - shortName: 'full_moon_with_face', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'bright', - 'face', - 'full', - 'moon', - 'uc6', - 'halloween', - 'space', - 'sky', - 'moon', - 'goodnight', - 'samhain', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'night', - 'moons', - 'lunar', - 'lunareclipse', - 'lunar eclipse' - ]), - Emoji( - name: 'first quarter moon face', - char: '\u{1F31B}', - shortName: 'first_quarter_moon_with_face', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'face', - 'moon', - 'quarter', - 'uc6', - 'space', - 'sky', - 'moon', - 'goodnight', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'night', - 'moons', - 'lunar', - 'lunareclipse', - 'lunar eclipse' - ]), - Emoji( - name: 'last quarter moon face', - char: '\u{1F31C}', - shortName: 'last_quarter_moon_with_face', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'face', - 'moon', - 'quarter', - 'uc6', - 'space', - 'sky', - 'moon', - 'goodnight', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'night', - 'moons', - 'lunar', - 'lunareclipse', - 'lunar eclipse' - ]), - Emoji( - name: 'new moon face', - char: '\u{1F31A}', - shortName: 'new_moon_with_face', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'face', - 'moon', - 'uc6', - 'space', - 'sky', - 'moon', - 'goodnight', - 'pokemon', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'night', - 'moons', - 'lunar', - 'lunareclipse', - 'lunar eclipse' - ]), - Emoji( - name: 'full moon', - char: '\u{1F315}', - shortName: 'full_moon', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'full', - 'moon', - 'uc6', - 'halloween', - 'space', - 'sky', - 'moon', - 'goodnight', - 'samhain', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'night', - 'moons', - 'lunar', - 'lunareclipse', - 'lunar eclipse' - ]), - Emoji( - name: 'waning gibbous moon', - char: '\u{1F316}', - shortName: 'waning_gibbous_moon', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'gibbous', - 'moon', - 'waning', - 'uc6', - 'space', - 'sky', - 'moon', - 'goodnight', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'night', - 'moons', - 'lunar', - 'lunareclipse', - 'lunar eclipse' - ]), - Emoji( - name: 'last quarter moon', - char: '\u{1F317}', - shortName: 'last_quarter_moon', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'moon', - 'quarter', - 'uc6', - 'space', - 'sky', - 'moon', - 'goodnight', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'night', - 'moons', - 'lunar', - 'lunareclipse', - 'lunar eclipse' - ]), - Emoji( - name: 'waning crescent moon', - char: '\u{1F318}', - shortName: 'waning_crescent_moon', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'crescent', - 'moon', - 'waning', - 'uc6', - 'space', - 'sky', - 'moon', - 'goodnight', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'night', - 'moons', - 'lunar', - 'lunareclipse', - 'lunar eclipse' - ]), - Emoji( - name: 'new moon', - char: '\u{1F311}', - shortName: 'new_moon', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'dark', - 'moon', - 'uc6', - 'halloween', - 'space', - 'sky', - 'moon', - 'goodnight', - 'samhain', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'night', - 'moons', - 'lunar', - 'lunareclipse', - 'lunar eclipse' - ]), - Emoji( - name: 'waxing crescent moon', - char: '\u{1F312}', - shortName: 'waxing_crescent_moon', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'crescent', - 'moon', - 'waxing', - 'uc6', - 'space', - 'sky', - 'moon', - 'goodnight', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'night', - 'moons', - 'lunar', - 'lunareclipse', - 'lunar eclipse' - ]), - Emoji( - name: 'first quarter moon', - char: '\u{1F313}', - shortName: 'first_quarter_moon', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'moon', - 'quarter', - 'uc6', - 'space', - 'sky', - 'moon', - 'goodnight', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'night', - 'moons', - 'lunar', - 'lunareclipse', - 'lunar eclipse' - ]), - Emoji( - name: 'waxing gibbous moon', - char: '\u{1F314}', - shortName: 'waxing_gibbous_moon', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'gibbous', - 'moon', - 'waxing', - 'uc6', - 'space', - 'sky', - 'moon', - 'goodnight', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'night', - 'moons', - 'lunar', - 'lunareclipse', - 'lunar eclipse' - ]), - Emoji( - name: 'crescent moon', - char: '\u{1F319}', - shortName: 'crescent_moon', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'crescent', - 'moon', - 'uc6', - 'space', - 'sky', - 'moon', - 'goodnight', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'night', - 'moons', - 'lunar', - 'lunareclipse', - 'lunar eclipse' - ]), - Emoji( - name: 'globe showing Americas', - char: '\u{1F30E}', - shortName: 'earth_americas', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeMap, - keywords: [ - 'Americas', - 'earth', - 'globe', - 'world', - 'uc6', - 'weather', - 'america', - 'space', - 'map', - 'vacation', - 'globe', - 'history', - 'world', - 'usa', - 'united states', - 'united states of america', - 'american', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'maps', - 'location', - 'locate', - 'local', - 'lost', - 'globes', - 'planet', - 'earth', - 'earthquake', - 'ancient', - 'old' - ]), - Emoji( - name: 'globe showing Europe-Africa', - char: '\u{1F30D}', - shortName: 'earth_africa', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeMap, - keywords: [ - 'Africa', - 'Europe', - 'earth', - 'globe', - 'world', - 'uc6', - 'space', - 'map', - 'vacation', - 'globe', - 'history', - 'world', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'maps', - 'location', - 'locate', - 'local', - 'lost', - 'globes', - 'planet', - 'earth', - 'earthquake', - 'ancient', - 'old' - ]), - Emoji( - name: 'globe showing Asia-Australia', - char: '\u{1F30F}', - shortName: 'earth_asia', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeMap, - keywords: [ - 'Asia', - 'Australia', - 'earth', - 'globe', - 'world', - 'uc6', - 'space', - 'map', - 'vacation', - 'globe', - 'history', - 'world', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'maps', - 'location', - 'locate', - 'local', - 'lost', - 'globes', - 'planet', - 'earth', - 'earthquake', - 'ancient', - 'old' - ]), - Emoji( - name: 'ringed planet', - char: '\u{1FA90}', - shortName: 'ringed_planet', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'uc12', - 'space', - 'saturn', - 'world', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'saturnine' - ]), - Emoji( - name: 'dizzy', - char: '\u{1F4AB}', - shortName: 'dizzy', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.emotion, - keywords: [ - 'comic', - 'star', - 'uc6', - 'star', - 'star wars', - 'drunk', - 'hit', - 'anime', - 'stars', - 'flustered', - 'dizzy', - 'punch', - 'pow', - 'bam', - 'manga' - ]), - Emoji( - name: 'star', - char: '\u{2B50}', - shortName: 'star', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'star', - 'uc5', - 'space', - 'sky', - 'star', - 'star wars', - 'fame', - 'texas', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'stars', - 'famous', - 'celebrity' - ]), - Emoji( - name: 'glowing star', - char: '\u{1F31F}', - shortName: 'star2', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'glittery', - 'glow', - 'shining', - 'sparkle', - 'star', - 'uc6', - 'space', - 'sky', - 'star', - 'christmas', - 'star wars', - 'fame', - 'sparkle', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'stars', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'famous', - 'celebrity', - 'bright', - 'shine', - 'twinkle' - ]), - Emoji( - name: 'sparkles', - char: '\u{2728}', - shortName: 'sparkles', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.event, - keywords: [ - 'sparkle', - 'star', - 'uc6', - 'star', - 'birthday', - 'girls night', - 'magic', - 'glitter', - 'bling', - 'fame', - 'sparkle', - 'stars', - 'birth', - 'cumpleaños', - 'anniversaire', - 'bday', - 'ladies night', - 'girls only', - 'girlfriend', - 'spell', - 'genie', - 'magical', - 'jewels', - 'gems', - 'jewel', - 'jewelry', - 'treasure', - 'famous', - 'celebrity', - 'bright', - 'shine', - 'twinkle' - ]), - Emoji( - name: 'high voltage', - char: '\u{26A1}', - shortName: 'zap', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'danger', - 'electric', - 'electricity', - 'lightning', - 'voltage', - 'zap', - 'uc4', - 'weather', - 'halloween', - 'sky', - 'diarrhea', - 'lightning', - 'electric', - 'harry potter', - 'magic', - 'power', - 'storm', - 'bling', - 'pokemon', - 'samhain', - 'shits', - 'the shits', - 'spell', - 'genie', - 'magical', - 'jewels', - 'gems', - 'jewel', - 'jewelry', - 'treasure' - ]), - Emoji( - name: 'comet', - char: '\u{2604}\u{FE0F}', - shortName: 'comet', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'space', - 'uc1', - 'space', - 'sky', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship' - ]), - Emoji( - name: 'collision', - char: '\u{1F4A5}', - shortName: 'boom', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.emotion, - keywords: [ - 'boom', - 'comic', - 'uc6', - 'blast', - 'explosion', - 'power', - 'flame', - 'anime', - 'sparkle', - 'boom', - 'explode', - 'burn', - 'match', - 'flames', - 'manga', - 'bright', - 'shine', - 'twinkle' - ]), - Emoji( - name: 'fire', - char: '\u{1F525}', - shortName: 'fire', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'flame', - 'tool', - 'uc6', - 'love', - 'christmas', - 'wth', - 'hot', - 'harry potter', - 'flame', - 'jewish', - 'porn', - 'independence day', - 'fires', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'what the hell', - 'heat', - 'warm', - 'caliente', - 'chaud', - 'heiß', - 'burn', - 'match', - 'flames', - 'hannukah', - 'hanukkah', - 'israel', - '4th of july' - ]), - Emoji( - name: 'tornado', - char: '\u{1F32A}\u{FE0F}', - shortName: 'cloud_tornado', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'cloud', - 'whirlwind', - 'uc7', - 'weather', - 'sky', - 'power', - 'storm', - 'clean', - 'texas' - ]), - Emoji( - name: 'rainbow', - char: '\u{1F308}', - shortName: 'rainbow', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'rain', - 'uc6', - 'weather', - 'gay', - 'sky', - 'rain', - 'rainbow', - 'gay pride', - 'hawaii', - 'color', - 'glitter', - 'easter', - 'irish', - 'mirror', - 'twink', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'colour', - 'coloring', - 'colouring', - 'drawing', - 'marker', - 'sketch', - 'saint patricks day', - 'st patricks day', - 'leprechaun' - ]), - Emoji( - name: 'sun', - char: '\u{2600}\u{FE0F}', - shortName: 'sunny', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'bright', - 'rays', - 'sunny', - 'uc1', - 'weather', - 'sun', - 'space', - 'sky', - 'day', - 'hot', - 'morning', - 'sunglasses', - 'power', - 'california', - 'las vegas', - 'summer', - 'independence day', - 'sunshine', - 'sunny', - 'eclipse', - 'solar', - 'solareclipse', - 'shiny', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'heat', - 'warm', - 'caliente', - 'chaud', - 'heiß', - 'good morning', - 'shades', - 'lunettes de soleil', - 'sun glasses', - 'vegas', - 'weekend', - '4th of july' - ]), - Emoji( - name: 'sun behind small cloud', - char: '\u{1F324}\u{FE0F}', - shortName: 'white_sun_small_cloud', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'cloud', - 'sun', - 'uc7', - 'weather', - 'sun', - 'sky', - 'cloud', - 'sunshine', - 'sunny', - 'eclipse', - 'solar', - 'solareclipse', - 'shiny', - 'clouds', - 'nuage' - ]), - Emoji( - name: 'sun behind cloud', - char: '\u{26C5}', - shortName: 'partly_sunny', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'cloud', - 'sun', - 'uc5', - 'weather', - 'sun', - 'sky', - 'cloud', - 'sunshine', - 'sunny', - 'eclipse', - 'solar', - 'solareclipse', - 'shiny', - 'clouds', - 'nuage' - ]), - Emoji( - name: 'sun behind large cloud', - char: '\u{1F325}\u{FE0F}', - shortName: 'white_sun_cloud', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'cloud', - 'sun', - 'uc7', - 'weather', - 'sun', - 'sky', - 'cloud', - 'cold', - 'sunshine', - 'sunny', - 'eclipse', - 'solar', - 'solareclipse', - 'shiny', - 'clouds', - 'nuage', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles' - ]), - Emoji( - name: 'cloud', - char: '\u{2601}\u{FE0F}', - shortName: 'cloud', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'weather', - 'uc1', - 'weather', - 'sky', - 'cloud', - 'cold', - 'dream', - 'clouds', - 'nuage', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles', - 'dreams' - ]), - Emoji( - name: 'sun behind rain cloud', - char: '\u{1F326}\u{FE0F}', - shortName: 'white_sun_rain_cloud', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'cloud', - 'rain', - 'sun', - 'uc7', - 'weather', - 'sun', - 'sky', - 'cloud', - 'rain', - 'cold', - 'sunshine', - 'sunny', - 'eclipse', - 'solar', - 'solareclipse', - 'shiny', - 'clouds', - 'nuage', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles' - ]), - Emoji( - name: 'cloud with rain', - char: '\u{1F327}\u{FE0F}', - shortName: 'cloud_rain', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'cloud', - 'rain', - 'uc7', - 'weather', - 'winter', - 'sky', - 'cloud', - 'rain', - 'cold', - 'clouds', - 'nuage', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles' - ]), - Emoji( - name: 'cloud with lightning and rain', - char: '\u{26C8}\u{FE0F}', - shortName: 'thunder_cloud_rain', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'cloud', - 'rain', - 'thunder', - 'uc5', - 'weather', - 'sky', - 'cloud', - 'rain', - 'cold', - 'lightning', - 'storm', - 'clouds', - 'nuage', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles' - ]), - Emoji( - name: 'cloud with lightning', - char: '\u{1F329}\u{FE0F}', - shortName: 'cloud_lightning', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'cloud', - 'lightning', - 'uc7', - 'weather', - 'halloween', - 'sky', - 'cloud', - 'rain', - 'cold', - 'lightning', - 'storm', - 'samhain', - 'clouds', - 'nuage', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles' - ]), - Emoji( - name: 'cloud with snow', - char: '\u{1F328}\u{FE0F}', - shortName: 'cloud_snow', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'cloud', - 'cold', - 'snow', - 'uc7', - 'weather', - 'winter', - 'sky', - 'cloud', - 'snow', - 'cold', - 'clouds', - 'nuage', - 'freeze', - 'frozen', - 'frost', - 'ice cube', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles' - ]), - Emoji( - name: 'snowflake', - char: '\u{2744}\u{FE0F}', - shortName: 'snowflake', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'cold', - 'snow', - 'uc1', - 'weather', - 'winter', - 'sky', - 'snow', - 'christmas', - 'cold', - 'freeze', - 'frozen', - 'frost', - 'ice cube', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles' - ]), - Emoji( - name: 'snowman', - char: '\u{2603}\u{FE0F}', - shortName: 'snowman2', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'cold', - 'snow', - 'uc1', - 'weather', - 'holidays', - 'winter', - 'snow', - 'christmas', - 'cold', - 'holiday', - 'freeze', - 'frozen', - 'frost', - 'ice cube', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles' - ]), - Emoji( - name: 'snowman without snow', - char: '\u{26C4}', - shortName: 'snowman', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'cold', - 'snow', - 'snowman', - 'uc5', - 'weather', - 'winter', - 'snow', - 'christmas', - 'cold', - 'freeze', - 'frozen', - 'frost', - 'ice cube', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles' - ]), - Emoji( - name: 'wind face', - char: '\u{1F32C}\u{FE0F}', - shortName: 'wind_blowing_face', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'blow', - 'cloud', - 'face', - 'wind', - 'uc7', - 'weather', - 'winter', - 'smoking', - 'cold', - 'power', - 'dream', - 'autumn', - 'breathe', - 'smoke', - 'cigarette', - 'puff', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles', - 'dreams', - 'fall', - 'sigh', - 'inhale' - ]), - Emoji( - name: 'dashing away', - char: '\u{1F4A8}', - shortName: 'dash', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.emotion, - keywords: [ - 'comic', - 'dash', - 'running', - 'uc6', - 'cloud', - 'smoking', - 'cold', - 'clouds', - 'nuage', - 'smoke', - 'cigarette', - 'puff', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles' - ]), - Emoji( - name: 'droplet', - char: '\u{1F4A7}', - shortName: 'droplet', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'cold', - 'comic', - 'drop', - 'sweat', - 'uc6', - 'weather', - 'sky', - 'rain', - 'sweat', - 'drip', - 'anime', - 'water', - 'manga', - 'water drop' - ]), - Emoji( - name: 'sweat droplets', - char: '\u{1F4A6}', - shortName: 'sweat_drops', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.emotion, - keywords: [ - 'comic', - 'splashing', - 'sweat', - 'uc6', - 'rain', - 'stressed', - 'sweat', - 'clean', - 'drip', - 'porn', - 'anime', - 'water', - 'manga', - 'water drop' - ]), - Emoji( - name: 'umbrella with rain drops', - char: '\u{2614}', - shortName: 'umbrella', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'clothing', - 'drop', - 'rain', - 'umbrella', - 'uc4', - 'weather', - 'sky', - 'rain', - 'cold', - 'umbrella', - 'england', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles', - 'united kingdom', - 'london', - 'uk' - ]), - Emoji( - name: 'umbrella', - char: '\u{2602}\u{FE0F}', - shortName: 'umbrella2', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'clothing', - 'rain', - 'uc1', - 'weather', - 'sky', - 'umbrella', - 'summer', - 'weekend' - ]), - Emoji( - name: 'water wave', - char: '\u{1F30A}', - shortName: 'ocean', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'ocean', - 'water', - 'wave', - 'uc6', - 'weather', - 'boat', - 'tropical', - 'swim', - 'hawaii', - 'storm', - 'mermaid', - 'california', - 'florida', - 'scuba', - 'waves', - 'ocean', - 'boats', - 'boating', - 'swimming', - 'swimmer', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'merboy', - 'mergirl', - 'merman', - 'merperson', - 'selkie', - 'undine', - 'atargatis', - 'siren', - 'snorkel', - 'sea' - ]), - Emoji( - name: 'fog', - char: '\u{1F32B}\u{FE0F}', - shortName: 'fog', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'cloud', - 'uc7', - 'weather', - 'sky', - 'cold', - 'steam', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles', - 'steaming', - 'piping' - ]), - Emoji( - name: 'green apple', - char: '\u{1F34F}', - shortName: 'green_apple', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodFruit, - keywords: [ - 'apple', - 'fruit', - 'green', - 'uc6', - 'food', - 'fruit', - 'classroom', - 'apples', - 'diet', - 'snacks', - 'picnic', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'snack' - ]), - Emoji( - name: 'red apple', - char: '\u{1F34E}', - shortName: 'apple', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodFruit, - keywords: [ - 'apple', - 'fruit', - 'red', - 'uc6', - 'food', - 'fruit', - 'classroom', - 'creationism', - 'new york', - 'apples', - 'diet', - 'snacks', - 'picnic', - 'vegetarian', - 'snow white', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'adam & eve', - 'adam and eve', - 'snack' - ]), - Emoji( - name: 'pear', - char: '\u{1F350}', - shortName: 'pear', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodFruit, - keywords: [ - 'fruit', - 'uc6', - 'food', - 'fruit', - 'diet', - 'picnic', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger' - ]), - Emoji( - name: 'tangerine', - char: '\u{1F34A}', - shortName: 'tangerine', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodFruit, - keywords: [ - 'fruit', - 'orange', - 'uc6', - 'food', - 'fruit', - 'breakfast', - 'diet', - 'donald trump', - 'florida', - 'citrus', - 'picnic', - 'orange', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'petit dejeuner', - 'trump', - 'juice', - 'lime' - ]), - Emoji( - name: 'lemon', - char: '\u{1F34B}', - shortName: 'lemon', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodFruit, - keywords: [ - 'citrus', - 'fruit', - 'uc6', - 'food', - 'fruit', - 'diet', - 'citrus', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'juice', - 'lime' - ]), - Emoji( - name: 'banana', - char: '\u{1F34C}', - shortName: 'banana', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodFruit, - keywords: [ - 'fruit', - 'uc6', - 'food', - 'fruit', - 'penis', - 'breakfast', - 'monkey', - 'diet', - 'snacks', - 'picnic', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'dick', - 'petit dejeuner', - 'progi', - 'ape', - 'primate', - 'snack' - ]), - Emoji( - name: 'watermelon', - char: '\u{1F349}', - shortName: 'watermelon', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodFruit, - keywords: [ - 'fruit', - 'uc6', - 'food', - 'fruit', - 'diet', - 'summer', - 'picnic', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'weekend' - ]), - Emoji( - name: 'grapes', - char: '\u{1F347}', - shortName: 'grapes', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodFruit, - keywords: [ - 'fruit', - 'grape', - 'uc6', - 'food', - 'fruit', - 'paris', - 'diet', - 'snacks', - 'picnic', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'french', - 'france', - 'snack' - ]), - Emoji( - name: 'blueberries', - char: '\u{1FAD0}', - shortName: 'blueberries', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodFruit, - keywords: [ - 'uc13', - 'food', - 'fruit', - 'breakfast', - 'diet', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'petit dejeuner' - ]), - Emoji( - name: 'strawberry', - char: '\u{1F353}', - shortName: 'strawberry', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodFruit, - keywords: [ - 'berry', - 'fruit', - 'uc6', - 'food', - 'fruit', - 'diet', - 'picnic', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger' - ]), - Emoji( - name: 'melon', - char: '\u{1F348}', - shortName: 'melon', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodFruit, - keywords: [ - 'fruit', - 'uc6', - 'food', - 'fruit', - 'boobs', - 'diet', - 'porn', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'boob', - 'tits', - 'tit', - 'breast' - ]), - Emoji( - name: 'cherries', - char: '\u{1F352}', - shortName: 'cherries', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodFruit, - keywords: [ - 'cherry', - 'fruit', - 'uc6', - 'food', - 'fruit', - 'sex', - 'vagina', - 'pussy', - 'diet', - 'porn', - 'picnic', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'fuck', - 'fucking', - 'horny', - 'humping', - 'condom' - ]), - Emoji( - name: 'peach', - char: '\u{1F351}', - shortName: 'peach', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodFruit, - keywords: [ - 'fruit', - 'uc6', - 'food', - 'fruit', - 'butt', - 'sex', - 'vagina', - 'pussy', - 'diet', - 'porn', - 'picnic', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'ass', - 'booty', - 'fuck', - 'fucking', - 'horny', - 'humping', - 'condom' - ]), - Emoji( - name: 'mango', - char: '\u{1F96D}', - shortName: 'mango', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodFruit, - keywords: [ - 'uc11', - 'food', - 'fruit', - 'tropical', - 'thai', - 'diet', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'pattaya' - ]), - Emoji( - name: 'pineapple', - char: '\u{1F34D}', - shortName: 'pineapple', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodFruit, - keywords: [ - 'fruit', - 'uc6', - 'food', - 'fruit', - 'tropical', - 'hawaii', - 'diet', - 'pineapple', - 'picnic', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'pinapple' - ]), - Emoji( - name: 'coconut', - char: '\u{1F965}', - shortName: 'coconut', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodFruit, - keywords: [ - 'palm', - 'piña colada', - 'uc10', - 'food', - 'fruit', - 'thai', - 'coconut', - 'diet', - 'vegetarian', - 'keto', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'pattaya' - ]), - Emoji( - name: 'kiwi fruit', - char: '\u{1F95D}', - shortName: 'kiwi', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodFruit, - keywords: [ - 'food', - 'fruit', - 'kiwi', - 'uc9', - 'food', - 'fruit', - 'breakfast', - 'diet', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'petit dejeuner' - ]), - Emoji( - name: 'tomato', - char: '\u{1F345}', - shortName: 'tomato', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodFruit, - keywords: [ - 'fruit', - 'vegetable', - 'uc6', - 'food', - 'fruit', - 'vegetables', - 'diet', - 'picnic', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'vegetable', - 'veggie', - 'legume' - ]), - Emoji( - name: 'eggplant', - char: '\u{1F346}', - shortName: 'eggplant', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodVegetable, - keywords: [ - 'aubergine', - 'vegetable', - 'uc6', - 'food', - 'vegetables', - 'penis', - 'sex', - 'diet', - 'porn', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'vegetable', - 'veggie', - 'legume', - 'dick', - 'fuck', - 'fucking', - 'horny', - 'humping' - ]), - Emoji( - name: 'avocado', - char: '\u{1F951}', - shortName: 'avocado', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodVegetable, - keywords: [ - 'avocado', - 'food', - 'fruit', - 'uc9', - 'food', - 'fruit', - 'vegetables', - 'california', - 'diet', - 'avocado', - 'picnic', - 'vegetarian', - 'keto', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'vegetable', - 'veggie', - 'legume', - 'avacado' - ]), - Emoji( - name: 'olive', - char: '\u{1FAD2}', - shortName: 'olive', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodFruit, - keywords: [ - 'uc13', - 'food', - 'italian', - 'vegetarian', - 'appetizer', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'italy', - 'italie', - 'apéro', - 'entrée' - ]), - Emoji( - name: 'broccoli', - char: '\u{1F966}', - shortName: 'broccoli', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodVegetable, - keywords: [ - 'wild cabbage', - 'uc10', - 'food', - 'vegetables', - 'diet', - 'vegetarian', - 'keto', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'vegetable', - 'veggie', - 'legume' - ]), - Emoji( - name: 'leafy green', - char: '\u{1F96C}', - shortName: 'leafy_green', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodVegetable, - keywords: [ - 'uc11', - 'food', - 'vegetables', - 'diet', - 'lettuce', - 'vegetarian', - 'keto', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'vegetable', - 'veggie', - 'legume' - ]), - Emoji( - name: 'bell pepper', - char: '\u{1FAD1}', - shortName: 'bell_pepper', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodVegetable, - keywords: [ - 'uc13', - 'food', - 'diet', - 'vegetarian', - 'appetizer', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'apéro', - 'entrée' - ]), - Emoji( - name: 'cucumber', - char: '\u{1F952}', - shortName: 'cucumber', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodVegetable, - keywords: [ - 'cucumber', - 'food', - 'pickle', - 'vegetable', - 'uc9', - 'food', - 'fruit', - 'vegetables', - 'penis', - 'diet', - 'pickle', - 'picnic', - 'vegetarian', - 'appetizer', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'vegetable', - 'veggie', - 'legume', - 'dick', - 'apéro', - 'entrée' - ]), - Emoji( - name: 'hot pepper', - char: '\u{1F336}\u{FE0F}', - shortName: 'hot_pepper', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodVegetable, - keywords: [ - 'hot', - 'pepper', - 'uc7', - 'food', - 'vegetables', - 'mexican', - 'hot', - 'chili', - 'diet', - 'texas', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'vegetable', - 'veggie', - 'legume', - 'mexico', - 'cinco de mayo', - 'español', - 'heat', - 'warm', - 'caliente', - 'chaud', - 'heiß' - ]), - Emoji( - name: 'ear of corn', - char: '\u{1F33D}', - shortName: 'corn', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodVegetable, - keywords: [ - 'corn', - 'ear', - 'maize', - 'maze', - 'uc6', - 'food', - 'vegetables', - 'diet', - 'farm', - 'picnic', - 'independence day', - 'thanksgiving', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'vegetable', - 'veggie', - 'legume', - '4th of july' - ]), - Emoji( - name: 'carrot', - char: '\u{1F955}', - shortName: 'carrot', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodVegetable, - keywords: [ - 'carrot', - 'food', - 'vegetable', - 'uc9', - 'food', - 'vegetables', - 'penis', - 'diet', - 'vegetarian', - 'appetizer', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'vegetable', - 'veggie', - 'legume', - 'dick', - 'apéro', - 'entrée' - ]), - Emoji( - name: 'garlic', - char: '\u{1F9C4}', - shortName: 'garlic', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodVegetable, - keywords: [ - 'uc12', - 'food', - 'vegetables', - 'diet', - 'vampire', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'vegetable', - 'veggie', - 'legume', - 'dracula' - ]), - Emoji( - name: 'onion', - char: '\u{1F9C5}', - shortName: 'onion', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodVegetable, - keywords: [ - 'uc12', - 'food', - 'cry', - 'vegetables', - 'diet', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'crying', - 'weeping', - 'weep', - 'sob', - 'sobbing', - 'tear', - 'tears', - 'bawling', - 'vegetable', - 'veggie', - 'legume' - ]), - Emoji( - name: 'potato', - char: '\u{1F954}', - shortName: 'potato', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodVegetable, - keywords: [ - 'food', - 'potato', - 'vegetable', - 'uc9', - 'food', - 'vegetables', - 'carbs', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'vegetable', - 'veggie', - 'legume', - 'carbohydrates' - ]), - Emoji( - name: 'roasted sweet potato', - char: '\u{1F360}', - shortName: 'sweet_potato', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodAsian, - keywords: [ - 'potato', - 'roasted', - 'sweet', - 'uc6', - 'food', - 'vegetables', - 'diet', - 'yam', - 'carbs', - 'thanksgiving', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'vegetable', - 'veggie', - 'legume', - 'carbohydrates' - ]), - Emoji( - name: 'croissant', - char: '\u{1F950}', - shortName: 'croissant', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'bread', - 'crescent roll', - 'croissant', - 'food', - 'french', - 'uc9', - 'food', - 'breakfast', - 'paris', - 'bake', - 'picnic', - 'carbs', - 'pastry', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'petit dejeuner', - 'french', - 'france', - 'baking', - 'carbohydrates', - 'pastries', - 'pâtisserie' - ]), - Emoji( - name: 'bagel', - char: '\u{1F96F}', - shortName: 'bagel', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'uc11', - 'food', - 'new york', - 'breakfast', - 'bake', - 'carbs', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'petit dejeuner', - 'baking', - 'carbohydrates' - ]), - Emoji( - name: 'bread', - char: '\u{1F35E}', - shortName: 'bread', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'loaf', - 'uc6', - 'food', - 'sandwich', - 'breakfast', - 'bake', - 'toast', - 'carbs', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'sanwiches', - 'petit dejeuner', - 'baking', - 'carbohydrates' - ]), - Emoji( - name: 'baguette bread', - char: '\u{1F956}', - shortName: 'french_bread', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'baguette', - 'bread', - 'food', - 'french', - 'uc9', - 'food', - 'penis', - 'paris', - 'bake', - 'picnic', - 'carbs', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'dick', - 'french', - 'france', - 'baking', - 'carbohydrates' - ]), - Emoji( - name: 'flatbread', - char: '\u{1FAD3}', - shortName: 'flatbread', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'uc13', - 'food', - 'carbs', - 'vegetarian', - 'pita', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'carbohydrates', - 'naan', - 'tortilla', - 'chepati', - 'focaccia', - 'fry bread', - 'lavash', - 'matzah', - 'roti' - ]), - Emoji( - name: 'pretzel', - char: '\u{1F968}', - shortName: 'pretzel', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'uc10', - 'food', - 'bake', - 'snacks', - 'carbs', - 'german', - 'vegetarian', - 'appetizer', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'baking', - 'snack', - 'carbohydrates', - 'oktoberfest', - 'octoberfest', - 'bratwurst', - 'apéro', - 'entrée' - ]), - Emoji( - name: 'cheese wedge', - char: '\u{1F9C0}', - shortName: 'cheese', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'cheese', - 'uc8', - 'food', - 'paris', - 'picnic', - 'cheese', - 'vegetarian', - 'keto', - 'appetizer', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'french', - 'france', - 'apéro', - 'entrée' - ]), - Emoji( - name: 'egg', - char: '\u{1F95A}', - shortName: 'egg', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'egg', - 'food', - 'uc9', - 'food', - 'breakfast', - 'easter', - 'diet', - 'eggs', - 'vegetarian', - 'keto', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'petit dejeuner' - ]), - Emoji( - name: 'cooking', - char: '\u{1F373}', - shortName: 'cooking', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'egg', - 'frying', - 'pan', - 'uc6', - 'food', - 'breakfast', - 'eggs', - 'restaurant', - 'vegetarian', - 'keto', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'petit dejeuner' - ]), - Emoji( - name: 'butter', - char: '\u{1F9C8}', - shortName: 'butter', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'uc12', - 'food', - 'breakfast', - 'condiment', - 'butter', - 'keto', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'petit dejeuner', - 'condiments', - 'seasoning', - 'topping', - 'margarine', - 'ghee' - ]), - Emoji( - name: 'pancakes', - char: '\u{1F95E}', - shortName: 'pancakes', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'crêpe', - 'food', - 'hotcake', - 'pancake', - 'uc9', - 'food', - 'breakfast', - 'carbs', - 'restaurant', - 'vegetarian', - 'pancake', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'petit dejeuner', - 'carbohydrates', - 'pannenkoeken', - 'maple syrup', - 'pfannkuchen', - 'panqueques', - 'crêpes' - ]), - Emoji( - name: 'waffle', - char: '\u{1F9C7}', - shortName: 'waffle', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'uc12', - 'food', - 'breakfast', - 'carbs', - 'waffles', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'petit dejeuner', - 'carbohydrates', - 'eggo', - 'gaufre', - 'gofre', - 'waffel', - 'wafel' - ]), - Emoji( - name: 'bacon', - char: '\u{1F953}', - shortName: 'bacon', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'bacon', - 'food', - 'meat', - 'uc9', - 'food', - 'breakfast', - 'pig', - 'meat', - 'restaurant', - 'keto', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'petit dejeuner', - 'pork' - ]), - Emoji( - name: 'cut of meat', - char: '\u{1F969}', - shortName: 'cut_of_meat', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'chop', - 'lambchop', - 'porkchop', - 'steak', - 'uc10', - 'food', - 'texas', - 'dinner', - 'steak', - 'meat', - 'restaurant', - 'keto', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'lunch' - ]), - Emoji( - name: 'poultry leg', - char: '\u{1F357}', - shortName: 'poultry_leg', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'bone', - 'chicken', - 'leg', - 'poultry', - 'uc6', - 'food', - 'chicken leg', - 'disney', - 'viking', - 'dinner', - 'meat', - 'thanksgiving', - 'restaurant', - 'keto', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'cartoon', - 'knight', - 'lunch' - ]), - Emoji( - name: 'meat on bone', - char: '\u{1F356}', - shortName: 'meat_on_bone', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'bone', - 'meat', - 'uc6', - 'food', - 'beef', - 'brazil', - 'viking', - 'dinner', - 'meat', - 'thanksgiving', - 'restaurant', - 'keto', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'brasil', - 'bresil', - 'knight', - 'lunch' - ]), - Emoji( - name: 'hot dog', - char: '\u{1F32D}', - shortName: 'hotdog', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'frankfurter', - 'hotdog', - 'sausage', - 'uc8', - 'food', - 'america', - 'new york', - 'sandwich', - 'dinner', - 'franks', - 'independence day', - 'german', - 'restaurant', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'usa', - 'united states', - 'united states of america', - 'american', - 'sanwiches', - 'lunch', - 'sausage', - 'hot dog', - '4th of july', - 'oktoberfest', - 'octoberfest', - 'bratwurst' - ]), - Emoji( - name: 'hamburger', - char: '\u{1F354}', - shortName: 'hamburger', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'burger', - 'uc6', - 'food', - 'america', - 'boys night', - 'sandwich', - 'beef', - 'mcdonalds', - 'dinner', - 'burger', - 'cheese', - 'independence day', - 'restaurant', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'usa', - 'united states', - 'united states of america', - 'american', - 'guys night', - 'sanwiches', - 'ronald mcdonald', - 'macdo', - 'lunch', - 'cheeseburger', - 'cheese burger', - '4th of july' - ]), - Emoji( - name: 'french fries', - char: '\u{1F35F}', - shortName: 'fries', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'french', - 'fries', - 'uc6', - 'food', - 'america', - 'chips', - 'mcdonalds', - 'dinner', - 'carbs', - 'restaurant', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'usa', - 'united states', - 'united states of america', - 'american', - 'ronald mcdonald', - 'macdo', - 'lunch', - 'carbohydrates' - ]), - Emoji( - name: 'pizza', - char: '\u{1F355}', - shortName: 'pizza', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'cheese', - 'slice', - 'uc6', - 'food', - 'italian', - 'boys night', - 'new york', - 'dinner', - 'cheese', - 'carbs', - 'restaurant', - 'keto', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'italy', - 'italie', - 'guys night', - 'lunch', - 'carbohydrates' - ]), - Emoji( - name: 'sandwich', - char: '\u{1F96A}', - shortName: 'sandwich', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'bread', - 'uc10', - 'food', - 'sandwich', - 'dinner', - 'restaurant', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'sanwiches', - 'lunch' - ]), - Emoji( - name: 'stuffed flatbread', - char: '\u{1F959}', - shortName: 'stuffed_flatbread', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'falafel', - 'flatbread', - 'food', - 'gyro', - 'kebab', - 'stuffed', - 'uc9', - 'food', - 'sandwich', - 'dinner', - 'german', - 'restaurant', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'sanwiches', - 'lunch', - 'oktoberfest', - 'octoberfest', - 'bratwurst' - ]), - Emoji( - name: 'falafel', - char: '\u{1F9C6}', - shortName: 'falafel', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'uc12', - 'food', - 'dinner', - 'meatball', - 'felafel', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'lunch', - 'chickpeas', - 'fava beans', - 'levantine', - 'meze' - ]), - Emoji( - name: 'taco', - char: '\u{1F32E}', - shortName: 'taco', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'mexican', - 'uc8', - 'food', - 'mexican', - 'vagina', - 'tacos', - 'hola', - 'pussy', - 'porn', - 'texas', - 'dinner', - 'restaurant', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'mexico', - 'cinco de mayo', - 'español', - 'condom', - 'lunch' - ]), - Emoji( - name: 'burrito', - char: '\u{1F32F}', - shortName: 'burrito', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'mexican', - 'wrap', - 'uc8', - 'food', - 'mexican', - 'hola', - 'texas', - 'dinner', - 'restaurant', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'mexico', - 'cinco de mayo', - 'español', - 'lunch' - ]), - Emoji( - name: 'tamale', - char: '\u{1FAD4}', - shortName: 'tamale', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'uc13', - 'food', - 'dinner', - 'tamal', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'lunch', - 'chuchito', - 'pastelle', - 'pasteles', - 'hallaca', - 'zacahuil', - 'corunda', - 'bollo', - 'humita', - 'binaki', - 'masa', - 'dukunu', - 'paches' - ]), - Emoji( - name: 'green salad', - char: '\u{1F957}', - shortName: 'salad', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'food', - 'green', - 'salad', - 'uc9', - 'food', - 'vegetables', - 'diet', - 'dinner', - 'lettuce', - 'picnic', - 'restaurant', - 'vegetarian', - 'keto', - 'appetizer', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'vegetable', - 'veggie', - 'legume', - 'lunch', - 'apéro', - 'entrée' - ]), - Emoji( - name: 'shallow pan of food', - char: '\u{1F958}', - shortName: 'shallow_pan_of_food', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'casserole', - 'food', - 'paella', - 'pan', - 'shallow', - 'uc9', - 'food', - 'mexican', - 'barcelona', - 'beef', - 'brazil', - 'dinner', - 'restaurant', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'mexico', - 'cinco de mayo', - 'español', - 'españa', - 'spanish', - 'brasil', - 'bresil', - 'lunch' - ]), - Emoji( - name: 'fondue', - char: '\u{1FAD5}', - shortName: 'fondue', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'uc13', - 'food', - 'dinner', - 'cheese', - 'vegetarian', - 'shabu', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'lunch', - 'hot pot' - ]), - Emoji( - name: 'canned food', - char: '\u{1F96B}', - shortName: 'canned_food', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'can', - 'uc10', - 'food', - 'soup', - 'dinner', - 'restaurant', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'lunch' - ]), - Emoji( - name: 'spaghetti', - char: '\u{1F35D}', - shortName: 'spaghetti', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodAsian, - keywords: [ - 'pasta', - 'uc6', - 'food', - 'noodles', - 'pasta', - 'italian', - 'dinner', - 'meatball', - 'restaurant', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'noodle', - 'pâtes', - 'italy', - 'italie', - 'lunch' - ]), - Emoji( - name: 'steaming bowl', - char: '\u{1F35C}', - shortName: 'ramen', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodAsian, - keywords: [ - 'bowl', - 'noodle', - 'ramen', - 'steaming', - 'uc6', - 'food', - 'noodles', - 'ramen', - 'pasta', - 'japan', - 'steam', - 'thai', - 'chinese', - 'soup', - 'dinner', - 'restaurant', - 'vegetarian', - 'bone broth', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'noodle', - 'pâtes', - 'japanese', - 'ninja', - 'steaming', - 'piping', - 'pattaya', - 'chinois', - 'asian', - 'chine', - 'lunch' - ]), - Emoji( - name: 'pot of food', - char: '\u{1F372}', - shortName: 'stew', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'pot', - 'stew', - 'uc6', - 'food', - 'steam', - 'thai', - 'brazil', - 'soup', - 'dinner', - 'stew', - 'thanksgiving', - 'restaurant', - 'bone broth', - 'shabu', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'steaming', - 'piping', - 'pattaya', - 'brasil', - 'bresil', - 'lunch', - 'braise', - 'hot pot' - ]), - Emoji( - name: 'curry rice', - char: '\u{1F35B}', - shortName: 'curry', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodAsian, - keywords: [ - 'curry', - 'rice', - 'uc6', - 'food', - 'japan', - 'thai', - 'dinner', - 'restaurant', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'japanese', - 'ninja', - 'pattaya', - 'lunch' - ]), - Emoji( - name: 'sushi', - char: '\u{1F363}', - shortName: 'sushi', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodAsian, - keywords: [ - 'sushi', - 'uc6', - 'food', - 'sushi', - 'japan', - 'california', - 'diet', - 'seafood', - 'dinner', - 'restaurant', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'japanese', - 'ninja', - 'lunch' - ]), - Emoji( - name: 'bento box', - char: '\u{1F371}', - shortName: 'bento', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodAsian, - keywords: [ - 'bento', - 'box', - 'uc6', - 'food', - 'sushi', - 'japan', - 'dinner', - 'restaurant', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'japanese', - 'ninja', - 'lunch' - ]), - Emoji( - name: 'dumpling', - char: '\u{1F95F}', - shortName: 'dumpling', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodAsian, - keywords: [ - 'empanada', - 'gyōza', - 'jiaozi', - 'pierogi', - 'potsticker', - 'uc10', - 'food', - 'chinese', - 'dinner', - 'dumpling', - 'pastry', - 'restaurant', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'chinois', - 'asian', - 'chine', - 'lunch', - 'Empanada', - 'Gyōza', - 'Pierogi', - 'pastries', - 'pâtisserie' - ]), - Emoji( - name: 'oyster', - char: '\u{1F9AA}', - shortName: 'oyster', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodMarine, - keywords: [ - 'uc12', - 'animal', - 'food', - 'seafood', - 'dinner', - 'ocean', - 'crustacean', - 'half shell', - 'animals', - 'animal kingdom', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'lunch', - 'sea', - 'pearl' - ]), - Emoji( - name: 'fried shrimp', - char: '\u{1F364}', - shortName: 'fried_shrimp', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodAsian, - keywords: [ - 'fried', - 'prawn', - 'shrimp', - 'tempura', - 'uc6', - 'food', - 'japan', - 'prawn', - 'seafood', - 'dinner', - 'crustacean', - 'restaurant', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'japanese', - 'ninja', - 'lunch' - ]), - Emoji( - name: 'rice ball', - char: '\u{1F359}', - shortName: 'rice_ball', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodAsian, - keywords: [ - 'Japanese', - 'ball', - 'rice', - 'uc6', - 'food', - 'sushi', - 'japan', - 'snacks', - 'dinner', - 'restaurant', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'japanese', - 'ninja', - 'snack', - 'lunch' - ]), - Emoji( - name: 'cooked rice', - char: '\u{1F35A}', - shortName: 'rice', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodAsian, - keywords: [ - 'cooked', - 'rice', - 'uc6', - 'food', - 'sushi', - 'japan', - 'thai', - 'chinese', - 'brazil', - 'dinner', - 'carbs', - 'restaurant', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'japanese', - 'ninja', - 'pattaya', - 'chinois', - 'asian', - 'chine', - 'brasil', - 'bresil', - 'lunch', - 'carbohydrates' - ]), - Emoji( - name: 'rice cracker', - char: '\u{1F358}', - shortName: 'rice_cracker', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodAsian, - keywords: [ - 'cracker', - 'rice', - 'uc6', - 'food', - 'sushi', - 'chinese', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'chinois', - 'asian', - 'chine' - ]), - Emoji( - name: 'fish cake with swirl', - char: '\u{1F365}', - shortName: 'fish_cake', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodAsian, - keywords: [ - 'cake', - 'fish', - 'pastry', - 'swirl', - 'uc6', - 'food', - 'sushi', - 'japan', - 'seafood', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'japanese', - 'ninja' - ]), - Emoji( - name: 'fortune cookie', - char: '\u{1F960}', - shortName: 'fortune_cookie', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodAsian, - keywords: [ - 'prophecy', - 'uc10', - 'food', - 'luck', - 'sugar', - 'cookie', - 'chinese', - 'bake', - 'carbs', - 'pastry', - 'restaurant', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'good luck', - 'lucky', - 'junk food', - 'dessert', - 'sweets', - 'cookies', - 'chinois', - 'asian', - 'chine', - 'baking', - 'carbohydrates', - 'pastries', - 'pâtisserie' - ]), - Emoji( - name: 'moon cake', - char: '\u{1F96E}', - shortName: 'moon_cake', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodAsian, - keywords: [ - 'uc11', - 'food', - 'cake', - 'celebrate', - 'sugar', - 'chinese', - 'bake', - 'carbs', - 'pastry', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'cupcake', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'junk food', - 'dessert', - 'sweets', - 'chinois', - 'asian', - 'chine', - 'baking', - 'carbohydrates', - 'pastries', - 'pâtisserie' - ]), - Emoji( - name: 'oden', - char: '\u{1F362}', - shortName: 'oden', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodAsian, - keywords: [ - 'kebab', - 'seafood', - 'skewer', - 'stick', - 'uc6', - 'food', - 'japan', - 'dinner', - 'restaurant', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'japanese', - 'ninja', - 'lunch' - ]), - Emoji( - name: 'dango', - char: '\u{1F361}', - shortName: 'dango', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodAsian, - keywords: [ - 'Japanese', - 'dessert', - 'skewer', - 'stick', - 'sweet', - 'uc6', - 'food', - 'japan', - 'sugar', - 'restaurant', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'japanese', - 'ninja', - 'junk food', - 'dessert', - 'sweets' - ]), - Emoji( - name: 'shaved ice', - char: '\u{1F367}', - shortName: 'shaved_ice', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodSweet, - keywords: [ - 'dessert', - 'ice', - 'shaved', - 'sweet', - 'uc6', - 'food', - 'ice cream', - 'hawaii', - 'sugar', - 'disney', - 'summer', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'junk food', - 'dessert', - 'sweets', - 'cartoon', - 'weekend' - ]), - Emoji( - name: 'ice cream', - char: '\u{1F368}', - shortName: 'ice_cream', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodSweet, - keywords: [ - 'cream', - 'dessert', - 'ice', - 'sweet', - 'uc6', - 'food', - 'ice cream', - 'sugar', - 'summer', - 'restaurant', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'junk food', - 'dessert', - 'sweets', - 'weekend' - ]), - Emoji( - name: 'soft ice cream', - char: '\u{1F366}', - shortName: 'icecream', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodSweet, - keywords: [ - 'cream', - 'dessert', - 'ice', - 'icecream', - 'soft', - 'sweet', - 'uc6', - 'food', - 'italian', - 'ice cream', - 'sugar', - 'summer', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'italy', - 'italie', - 'junk food', - 'dessert', - 'sweets', - 'weekend' - ]), - Emoji( - name: 'pie', - char: '\u{1F967}', - shortName: 'pie', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodSweet, - keywords: [ - 'uc10', - 'food', - 'sugar', - 'bake', - 'carbs', - 'pastry', - 'quiche', - 'thanksgiving', - 'restaurant', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'junk food', - 'dessert', - 'sweets', - 'baking', - 'carbohydrates', - 'pastries', - 'pâtisserie', - 'tart' - ]), - Emoji( - name: 'cupcake', - char: '\u{1F9C1}', - shortName: 'cupcake', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodSweet, - keywords: [ - 'uc11', - 'food', - 'birthday', - 'happy birthday', - 'cake', - 'pink', - 'celebrate', - 'sugar', - 'bake', - 'carbs', - 'pastry', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'birth', - 'cumpleaños', - 'anniversaire', - 'bday', - 'Bon anniversaire', - 'joyeux anniversaire', - 'buon compleanno', - 'feliz cumpleaños', - 'alles Gute zum Geburtstag', - 'feliz Aniversário', - 'Gratulerer med dagen', - 'cupcake', - 'rose', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'junk food', - 'dessert', - 'sweets', - 'baking', - 'carbohydrates', - 'pastries', - 'pâtisserie' - ]), - Emoji( - name: 'shortcake', - char: '\u{1F370}', - shortName: 'cake', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodSweet, - keywords: [ - 'cake', - 'dessert', - 'pastry', - 'slice', - 'sweet', - 'uc6', - 'food', - 'cake', - 'sugar', - 'bake', - 'carbs', - 'pastry', - 'restaurant', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'cupcake', - 'junk food', - 'dessert', - 'sweets', - 'baking', - 'carbohydrates', - 'pastries', - 'pâtisserie' - ]), - Emoji( - name: 'birthday cake', - char: '\u{1F382}', - shortName: 'birthday', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodSweet, - keywords: [ - 'birthday', - 'cake', - 'celebration', - 'dessert', - 'pastry', - 'sweet', - 'uc6', - 'food', - 'holidays', - 'birthday', - 'happy birthday', - 'cake', - 'celebrate', - 'sugar', - 'facebook', - 'bake', - 'pastry', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'holiday', - 'birth', - 'cumpleaños', - 'anniversaire', - 'bday', - 'Bon anniversaire', - 'joyeux anniversaire', - 'buon compleanno', - 'feliz cumpleaños', - 'alles Gute zum Geburtstag', - 'feliz Aniversário', - 'Gratulerer med dagen', - 'cupcake', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'junk food', - 'dessert', - 'sweets', - 'baking', - 'pastries', - 'pâtisserie' - ]), - Emoji( - name: 'custard', - char: '\u{1F36E}', - shortName: 'custard', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodSweet, - keywords: [ - 'dessert', - 'pudding', - 'sweet', - 'uc6', - 'food', - 'sugar', - 'bake', - 'restaurant', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'junk food', - 'dessert', - 'sweets', - 'baking' - ]), - Emoji( - name: 'lollipop', - char: '\u{1F36D}', - shortName: 'lollipop', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodSweet, - keywords: [ - 'candy', - 'dessert', - 'sweet', - 'uc6', - 'food', - 'halloween', - 'candy', - 'sugar', - 'disney', - 'snacks', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'samhain', - 'candy cane', - 'candycane', - 'lolipop', - 'bonbon', - 'junk food', - 'dessert', - 'sweets', - 'cartoon', - 'snack' - ]), - Emoji( - name: 'candy', - char: '\u{1F36C}', - shortName: 'candy', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodSweet, - keywords: [ - 'dessert', - 'sweet', - 'uc6', - 'food', - 'halloween', - 'candy', - 'sugar', - 'snacks', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'samhain', - 'candy cane', - 'candycane', - 'lolipop', - 'bonbon', - 'junk food', - 'dessert', - 'sweets', - 'snack' - ]), - Emoji( - name: 'chocolate bar', - char: '\u{1F36B}', - shortName: 'chocolate_bar', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodSweet, - keywords: [ - 'bar', - 'chocolate', - 'dessert', - 'sweet', - 'uc6', - 'food', - 'halloween', - 'love', - 'girls night', - 'candy', - 'sugar', - 'easter', - 'cocoa', - 'snacks', - 'rich', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'samhain', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'ladies night', - 'girls only', - 'girlfriend', - 'candy cane', - 'candycane', - 'lolipop', - 'bonbon', - 'junk food', - 'dessert', - 'sweets', - 'hot chocolate', - 'snack', - 'grand', - 'expensive', - 'fancy' - ]), - Emoji( - name: 'popcorn', - char: '\u{1F37F}', - shortName: 'popcorn', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'popcorn', - 'uc8', - 'food', - 'celebrate', - 'snacks', - 'carbs', - 'vegetarian', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'snack', - 'carbohydrates' - ]), - Emoji( - name: 'doughnut', - char: '\u{1F369}', - shortName: 'doughnut', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodSweet, - keywords: [ - 'dessert', - 'donut', - 'sweet', - 'uc6', - 'food', - 'sex', - 'vagina', - 'breakfast', - 'doughnut', - 'sugar', - 'bake', - 'carbs', - 'pastry', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'fuck', - 'fucking', - 'horny', - 'humping', - 'petit dejeuner', - 'donut', - 'junk food', - 'dessert', - 'sweets', - 'baking', - 'carbohydrates', - 'pastries', - 'pâtisserie' - ]), - Emoji( - name: 'cookie', - char: '\u{1F36A}', - shortName: 'cookie', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodSweet, - keywords: [ - 'dessert', - 'sweet', - 'uc6', - 'food', - 'christmas', - 'vagina', - 'sugar', - 'cookie', - 'bake', - 'snacks', - 'carbs', - 'pastry', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'junk food', - 'dessert', - 'sweets', - 'cookies', - 'baking', - 'snack', - 'carbohydrates', - 'pastries', - 'pâtisserie' - ]), - Emoji( - name: 'chestnut', - char: '\u{1F330}', - shortName: 'chestnut', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodVegetable, - keywords: [ - 'plant', - 'uc6', - 'food', - 'nature', - 'plant', - 'christmas', - 'nut', - 'keto', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'plants', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'nuts' - ]), - Emoji( - name: 'peanuts', - char: '\u{1F95C}', - shortName: 'peanuts', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodVegetable, - keywords: [ - 'food', - 'nut', - 'peanut', - 'vegetable', - 'uc9', - 'food', - 'vegetables', - 'squirrel', - 'nut', - 'snacks', - 'picnic', - 'appetizer', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'vegetable', - 'veggie', - 'legume', - 'nuts', - 'snack', - 'apéro', - 'entrée' - ]), - Emoji( - name: 'honey pot', - char: '\u{1F36F}', - shortName: 'honey_pot', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodSweet, - keywords: [ - 'honey', - 'honeypot', - 'pot', - 'sweet', - 'uc6', - 'food', - 'vagina', - 'breakfast', - 'sugar', - 'condiment', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'petit dejeuner', - 'junk food', - 'dessert', - 'sweets', - 'condiments', - 'seasoning', - 'topping' - ]), - Emoji( - name: 'glass of milk', - char: '\u{1F95B}', - shortName: 'milk', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.drink, - keywords: [ - 'drink', - 'glass', - 'milk', - 'uc9', - 'drink', - 'christmas', - 'dinner', - 'restaurant', - 'drinks', - 'beverage', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'lunch' - ]), - Emoji( - name: 'baby bottle', - char: '\u{1F37C}', - shortName: 'baby_bottle', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.drink, - keywords: [ - 'baby', - 'bottle', - 'drink', - 'milk', - 'uc6', - 'food', - 'drink', - 'baby', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'drinks', - 'beverage', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino' - ]), - Emoji( - name: 'hot beverage', - char: '\u{2615}', - shortName: 'coffee', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.drink, - keywords: [ - 'beverage', - 'coffee', - 'drink', - 'hot', - 'steaming', - 'tea', - 'uc4', - 'drink', - 'caffeine', - 'steam', - 'morning', - 'coffee', - 'breakfast', - 'cocoa', - 'diet', - 'restaurant', - 'keto', - 'drinks', - 'beverage', - 'decaffeinated', - 'decaf', - 'steaming', - 'piping', - 'good morning', - 'starbucks', - 'petit dejeuner', - 'hot chocolate' - ]), - Emoji( - name: 'teacup without handle', - char: '\u{1F375}', - shortName: 'tea', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.drink, - keywords: [ - 'beverage', - 'cup', - 'drink', - 'tea', - 'teacup', - 'uc6', - 'drink', - 'japan', - 'caffeine', - 'steam', - 'morning', - 'tea', - 'breakfast', - 'england', - 'chinese', - 'diet', - 'restaurant', - 'keto', - 'drinks', - 'beverage', - 'japanese', - 'ninja', - 'decaffeinated', - 'decaf', - 'steaming', - 'piping', - 'good morning', - 'iced tea', - 'petit dejeuner', - 'united kingdom', - 'london', - 'uk', - 'chinois', - 'asian', - 'chine' - ]), - Emoji( - name: 'teapot', - char: '\u{1FAD6}', - shortName: 'teapot', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.drink, - keywords: [ - 'uc13', - 'japan', - 'breakfast', - 'chinese', - 'kettle', - 'japanese', - 'ninja', - 'petit dejeuner', - 'chinois', - 'asian', - 'chine', - 'teakettle', - 'caldron', - 'boiler', - 'théière', - 'teiera', - 'tetera', - 'infuser', - 'kyūsu', - 'tetsubin', - 'yixing' - ]), - Emoji( - name: 'mate', - char: '\u{1F9C9}', - shortName: 'mate', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.drink, - keywords: [ - 'uc12', - 'drink', - 'caffeine', - 'tea', - 'breakfast', - 'yerba', - 'drinks', - 'beverage', - 'decaffeinated', - 'decaf', - 'iced tea', - 'petit dejeuner', - 'chimarrão', - 'cimarrón', - 'maté' - ]), - Emoji( - name: 'bubble tea', - char: '\u{1F9CB}', - shortName: 'bubble_tea', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.drink, - keywords: [ - 'uc13', - 'drink', - 'japan', - 'thai', - 'chinese', - 'drinks', - 'beverage', - 'japanese', - 'ninja', - 'pattaya', - 'chinois', - 'asian', - 'chine' - ]), - Emoji( - name: 'beverage box', - char: '\u{1F9C3}', - shortName: 'beverage_box', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.drink, - keywords: [ - 'uc12', - 'drink', - 'apples', - 'citrus', - 'snacks', - 'drinks', - 'beverage', - 'juice', - 'lime', - 'snack' - ]), - Emoji( - name: 'cup with straw', - char: '\u{1F964}', - shortName: 'cup_with_straw', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.drink, - keywords: [ - 'uc10', - 'drink', - 'dinner', - 'soda', - 'restaurant', - 'keto', - 'drinks', - 'beverage', - 'lunch', - 'cola', - 'milkshake', - 'soft drink', - 'sippy cup', - 'coke', - 'pepsi' - ]), - Emoji( - name: 'sake', - char: '\u{1F376}', - shortName: 'sake', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.drink, - keywords: [ - 'bar', - 'beverage', - 'bottle', - 'cup', - 'drink', - 'uc6', - 'drink', - 'japan', - 'alcohol', - 'sake', - 'girls night', - 'chinese', - 'dinner', - 'restaurant', - 'drinks', - 'beverage', - 'japanese', - 'ninja', - 'liquor', - 'booze', - 'ladies night', - 'girls only', - 'girlfriend', - 'chinois', - 'asian', - 'chine', - 'lunch' - ]), - Emoji( - name: 'beer mug', - char: '\u{1F37A}', - shortName: 'beer', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.drink, - keywords: [ - 'bar', - 'beer', - 'drink', - 'mug', - 'uc6', - 'drink', - 'japan', - 'alcohol', - 'beer', - 'cocktail', - 'friend', - 'irish', - 'dinner', - 'german', - 'restaurant', - 'drinks', - 'beverage', - 'japanese', - 'ninja', - 'liquor', - 'booze', - 'martini', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'saint patricks day', - 'st patricks day', - 'leprechaun', - 'lunch', - 'oktoberfest', - 'octoberfest', - 'bratwurst' - ]), - Emoji( - name: 'clinking beer mugs', - char: '\u{1F37B}', - shortName: 'beers', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.drink, - keywords: [ - 'bar', - 'beer', - 'clink', - 'drink', - 'mug', - 'uc6', - 'drink', - 'alcohol', - 'cheers', - 'beer', - 'cocktail', - 'thank you', - 'girls night', - 'boys night', - 'harry potter', - 'friend', - 'celebrate', - 'irish', - 'toast', - 'german', - 'restaurant', - 'drinks', - 'beverage', - 'liquor', - 'booze', - 'gān bēi', - 'Na zdravi', - 'Proost', - 'Prost', - 'Sláinte', - 'Cin cin', - 'Kanpai', - 'Na zdrowie', - 'Saúde', - 'На здоровье', - 'Salud', - 'Skål', - 'Sei gesund', - 'santé', - 'martini', - 'thanks', - 'thankful', - 'praise', - 'gracias', - 'merci', - 'thankyou', - 'acceptable', - 'ladies night', - 'girls only', - 'girlfriend', - 'guys night', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'saint patricks day', - 'st patricks day', - 'leprechaun', - 'oktoberfest', - 'octoberfest', - 'bratwurst' - ]), - Emoji( - name: 'clinking glasses', - char: '\u{1F942}', - shortName: 'champagne_glass', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.drink, - keywords: [ - 'celebrate', - 'clink', - 'drink', - 'glass', - 'uc9', - 'drink', - 'alcohol', - 'cheers', - 'girls night', - 'friend', - 'celebrate', - 'toast', - 'restaurant', - 'drinks', - 'beverage', - 'liquor', - 'booze', - 'gān bēi', - 'Na zdravi', - 'Proost', - 'Prost', - 'Sláinte', - 'Cin cin', - 'Kanpai', - 'Na zdrowie', - 'Saúde', - 'На здоровье', - 'Salud', - 'Skål', - 'Sei gesund', - 'santé', - 'ladies night', - 'girls only', - 'girlfriend', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar' - ]), - Emoji( - name: 'wine glass', - char: '\u{1F377}', - shortName: 'wine_glass', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.drink, - keywords: [ - 'bar', - 'beverage', - 'drink', - 'glass', - 'wine', - 'uc6', - 'drink', - 'italian', - 'christmas', - 'alcohol', - 'cocktail', - 'girls night', - 'australia', - 'paris', - 'rich', - 'dinner', - 'picnic', - 'thanksgiving', - 'restaurant', - 'drinks', - 'beverage', - 'italy', - 'italie', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'liquor', - 'booze', - 'martini', - 'ladies night', - 'girls only', - 'girlfriend', - 'french', - 'france', - 'grand', - 'expensive', - 'fancy', - 'lunch' - ]), - Emoji( - name: 'tumbler glass', - char: '\u{1F943}', - shortName: 'tumbler_glass', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.drink, - keywords: [ - 'glass', - 'liquor', - 'shot', - 'tumbler', - 'whisky', - 'uc9', - 'drink', - 'japan', - 'alcohol', - 'cocktail', - 'boys night', - 'whisky', - 'irish', - 'scotland', - 'las vegas', - 'dinner', - 'shot', - 'restaurant', - 'keto', - 'drinks', - 'beverage', - 'japanese', - 'ninja', - 'liquor', - 'booze', - 'martini', - 'guys night', - 'whiskey', - 'scotch', - 'saint patricks day', - 'st patricks day', - 'leprechaun', - 'scottish', - 'vegas', - 'lunch' - ]), - Emoji( - name: 'cocktail glass', - char: '\u{1F378}', - shortName: 'cocktail', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.drink, - keywords: [ - 'bar', - 'cocktail', - 'drink', - 'glass', - 'uc6', - 'drink', - 'alcohol', - 'cocktail', - 'girls night', - 'las vegas', - 'dinner', - 'restaurant', - 'drinks', - 'beverage', - 'liquor', - 'booze', - 'martini', - 'ladies night', - 'girls only', - 'girlfriend', - 'vegas', - 'lunch' - ]), - Emoji( - name: 'tropical drink', - char: '\u{1F379}', - shortName: 'tropical_drink', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.drink, - keywords: [ - 'bar', - 'drink', - 'tropical', - 'uc6', - 'drink', - 'alcohol', - 'tropical', - 'cocktail', - 'tea', - 'citrus', - 'summer', - 'dinner', - 'restaurant', - 'drinks', - 'beverage', - 'liquor', - 'booze', - 'martini', - 'iced tea', - 'juice', - 'lime', - 'weekend', - 'lunch' - ]), - Emoji( - name: 'bottle with popping cork', - char: '\u{1F37E}', - shortName: 'champagne', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.drink, - keywords: [ - 'bar', - 'bottle', - 'cork', - 'drink', - 'popping', - 'uc8', - 'drink', - 'holidays', - 'alcohol', - 'cheers', - 'celebrate', - 'paris', - 'rich', - 'toast', - 'picnic', - 'restaurant', - 'drinks', - 'beverage', - 'holiday', - 'liquor', - 'booze', - 'gān bēi', - 'Na zdravi', - 'Proost', - 'Prost', - 'Sláinte', - 'Cin cin', - 'Kanpai', - 'Na zdrowie', - 'Saúde', - 'На здоровье', - 'Salud', - 'Skål', - 'Sei gesund', - 'santé', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'french', - 'france', - 'grand', - 'expensive', - 'fancy' - ]), - Emoji( - name: 'ice', - char: '\u{1F9CA}', - shortName: 'ice_cube', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.drink, - keywords: [ - 'uc12', - 'snow', - 'cold', - 'igloo', - 'cubo de hielo', - 'freeze', - 'frozen', - 'frost', - 'ice cube', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles', - 'glaçon', - 'cubetto di ghiaccio' - ]), - Emoji( - name: 'spoon', - char: '\u{1F944}', - shortName: 'spoon', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.dishware, - keywords: [ - 'spoon', - 'tableware', - 'uc9', - 'food', - 'cutlery', - 'steel', - 'utensils', - 'restaurant', - 'dishes', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'dish', - 'metal' - ]), - Emoji( - name: 'fork and knife', - char: '\u{1F374}', - shortName: 'fork_and_knife', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.dishware, - keywords: [ - 'cooking', - 'fork', - 'knife', - 'uc6', - 'food', - 'christmas', - 'cutlery', - 'dinner', - 'steel', - 'picnic', - 'independence day', - 'utensils', - 'restaurant', - 'dishes', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'dish', - 'lunch', - 'metal', - '4th of july' - ]), - Emoji( - name: 'fork and knife with plate', - char: '\u{1F37D}\u{FE0F}', - shortName: 'fork_knife_plate', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.dishware, - keywords: [ - 'cooking', - 'fork', - 'knife', - 'plate', - 'uc7', - 'food', - 'cutlery', - 'diet', - 'dinner', - 'picnic', - 'utensils', - 'restaurant', - 'dishes', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'dish', - 'lunch' - ]), - Emoji( - name: 'bowl with spoon', - char: '\u{1F963}', - shortName: 'bowl_with_spoon', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'uc10', - 'food', - 'breakfast', - 'soup', - 'dinner', - 'cereal', - 'restaurant', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'petit dejeuner', - 'lunch' - ]), - Emoji( - name: 'takeout box', - char: '\u{1F961}', - shortName: 'takeout_box', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodAsian, - keywords: [ - 'oyster pail', - 'uc10', - 'food', - 'chinese', - 'dinner', - 'oyster pail', - 'restaurant', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'chinois', - 'asian', - 'chine', - 'lunch' - ]), - Emoji( - name: 'chopsticks', - char: '\u{1F962}', - shortName: 'chopsticks', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.dishware, - keywords: [ - 'uc10', - 'food', - 'sushi', - 'japan', - 'chinese', - 'utensils', - 'restaurant', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'japanese', - 'ninja', - 'chinois', - 'asian', - 'chine' - ]), - Emoji( - name: 'salt', - char: '\u{1F9C2}', - shortName: 'salt', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.foodPrepared, - keywords: [ - 'uc11', - 'condiment', - 'restaurant', - 'condiments', - 'seasoning', - 'topping' - ]), - Emoji( - name: 'soccer ball', - char: '\u{26BD}', - shortName: 'soccer', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'ball', - 'football', - 'soccer', - 'uc5', - 'sport', - 'game', - 'ball', - 'football', - 'soccer', - 'play', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'games', - 'gaming', - 'balls', - 'ballon', - 'soccer ball', - 'world cup' - ]), - Emoji( - name: 'basketball', - char: '\u{1F3C0}', - shortName: 'basketball', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'ball', - 'hoop', - 'uc6', - 'sport', - 'game', - 'ball', - 'basketball', - 'play', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'games', - 'gaming', - 'balls', - 'ballon' - ]), - Emoji( - name: 'american football', - char: '\u{1F3C8}', - shortName: 'football', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'american', - 'ball', - 'football', - 'uc6', - 'sport', - 'america', - 'game', - 'ball', - 'football', - 'play', - 'texas', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'usa', - 'united states', - 'united states of america', - 'american', - 'games', - 'gaming', - 'balls', - 'ballon' - ]), - Emoji( - name: 'baseball', - char: '\u{26BE}', - shortName: 'baseball', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'ball', - 'uc5', - 'sport', - 'game', - 'ball', - 'play', - 'throw', - 'activity', - 'independence day', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'games', - 'gaming', - 'balls', - 'ballon', - '4th of july' - ]), - Emoji( - name: 'softball', - char: '\u{1F94E}', - shortName: 'softball', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'uc11', - 'sport', - 'game', - 'ball', - 'play', - 'fun', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'games', - 'gaming', - 'balls', - 'ballon' - ]), - Emoji( - name: 'tennis', - char: '\u{1F3BE}', - shortName: 'tennis', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'ball', - 'racquet', - 'uc6', - 'sport', - 'game', - 'ball', - 'tennis', - 'play', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'games', - 'gaming', - 'balls', - 'ballon', - 'tennis ball', - 'tennis racquet' - ]), - Emoji( - name: 'volleyball', - char: '\u{1F3D0}', - shortName: 'volleyball', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'ball', - 'game', - 'uc8', - 'sport', - 'game', - 'ball', - 'volley ball', - 'play', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'games', - 'gaming', - 'balls', - 'ballon' - ]), - Emoji( - name: 'rugby football', - char: '\u{1F3C9}', - shortName: 'rugby_football', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'ball', - 'football', - 'rugby', - 'uc6', - 'sport', - 'game', - 'ball', - 'football', - 'play', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'games', - 'gaming', - 'balls', - 'ballon' - ]), - Emoji( - name: 'flying disc', - char: '\u{1F94F}', - shortName: 'flying_disc', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'uc11', - 'sport', - 'game', - 'play', - 'fun', - 'throw', - 'activity', - 'frisbee', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'games', - 'gaming', - 'disque-volant', - 'boomerang' - ]), - Emoji( - name: 'boomerang', - char: '\u{1FA83}', - shortName: 'boomerang', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.tool, - keywords: [ - 'uc13', - 'sport', - 'game', - 'play', - 'fun', - 'throw', - 'hunt', - 'activity', - 'boumerang', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'games', - 'gaming', - 'airfoil', - 'aerofoil' - ]), - Emoji( - name: 'pool 8 ball', - char: '\u{1F3B1}', - shortName: '8ball', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.game, - keywords: [ - '8', - '8 ball', - 'ball', - 'billiard', - 'eight', - 'game', - 'uc6', - 'sport', - 'game', - 'ball', - 'billiards', - 'luck', - 'boys night', - 'play', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'games', - 'gaming', - 'balls', - 'ballon', - 'billiards ball', - '8 ball', - '8ball', - 'pool', - 'eight ball', - 'good luck', - 'lucky', - 'guys night' - ]), - Emoji( - name: 'yo-yo', - char: '\u{1FA80}', - shortName: 'yo_yo', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.game, - keywords: [ - 'uc12', - 'game', - 'play', - 'activity', - 'yoyo', - 'toy', - 'stringed', - 'games', - 'gaming', - 'fluctuate' - ]), - Emoji( - name: 'ping pong', - char: '\u{1F3D3}', - shortName: 'ping_pong', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'ball', - 'bat', - 'game', - 'paddle', - 'ping pong', - 'table tennis', - 'uc8', - 'sport', - 'game', - 'ball', - 'ping pong', - 'play', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'games', - 'gaming', - 'balls', - 'ballon', - 'table tennis', - 'ping pong ball', - 'ping pong paddle', - 'paddle' - ]), - Emoji( - name: 'badminton', - char: '\u{1F3F8}', - shortName: 'badminton', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'birdie', - 'game', - 'racquet', - 'shuttlecock', - 'uc8', - 'sport', - 'game', - 'play', - 'fun', - 'activity', - 'stringed', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'games', - 'gaming' - ]), - Emoji( - name: 'ice hockey', - char: '\u{1F3D2}', - shortName: 'hockey', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'game', - 'hockey', - 'ice', - 'puck', - 'stick', - 'uc8', - 'sport', - 'game', - 'hockey', - 'play', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'games', - 'gaming', - 'field hockey' - ]), - Emoji( - name: 'field hockey', - char: '\u{1F3D1}', - shortName: 'field_hockey', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'ball', - 'field', - 'game', - 'hockey', - 'stick', - 'uc8', - 'sport', - 'game', - 'ball', - 'hockey', - 'play', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'games', - 'gaming', - 'balls', - 'ballon', - 'field hockey' - ]), - Emoji( - name: 'lacrosse', - char: '\u{1F94D}', - shortName: 'lacrosse', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'uc11', - 'sport', - 'game', - 'ball', - 'play', - 'fun', - 'throw', - 'activity', - 'stringed', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'games', - 'gaming', - 'balls', - 'ballon' - ]), - Emoji( - name: 'cricket game', - char: '\u{1F3CF}', - shortName: 'cricket_game', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'ball', - 'bat', - 'game', - 'uc8', - 'sport', - 'game', - 'ball', - 'cricket', - 'play', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'games', - 'gaming', - 'balls', - 'ballon', - 'cricket bat', - 'cricket ball' - ]), - Emoji( - name: 'goal net', - char: '\u{1F945}', - shortName: 'goal', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'goal', - 'net', - 'uc9', - 'sport', - 'football', - 'soccer', - 'play', - 'fun', - 'activity', - 'stringed', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'soccer ball', - 'world cup' - ]), - Emoji( - name: 'flag in hole', - char: '\u{26F3}', - shortName: 'golf', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'golf', - 'hole', - 'uc5', - 'sport', - 'game', - 'ball', - 'vacation', - 'golf', - 'play', - 'florida', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'games', - 'gaming', - 'balls', - 'ballon', - 'golfing', - 'golfer' - ]), - Emoji( - name: 'kite', - char: '\u{1FA81}', - shortName: 'kite', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.game, - keywords: [ - 'uc12', - 'sport', - 'fly', - 'vacation', - 'fun', - 'activity', - 'toy', - 'kite', - 'stringed', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'flight', - 'flying', - 'flights', - 'avion' - ]), - Emoji( - name: 'bow and arrow', - char: '\u{1F3F9}', - shortName: 'bow_and_arrow', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.tool, - keywords: [ - 'Sagittarius', - 'archer', - 'archery', - 'arrow', - 'bow', - 'tool', - 'weapon', - 'zodiac', - 'uc8', - 'sport', - 'weapon', - 'arrow', - 'game', - 'play', - 'target', - 'minecraft', - 'activity', - 'stringed', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'weapons', - 'arrows', - 'games', - 'gaming' - ]), - Emoji( - name: 'fishing pole', - char: '\u{1F3A3}', - shortName: 'fishing_pole_and_fish', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'fish', - 'pole', - 'uc6', - 'sport', - 'vacation', - 'fishing', - 'florida', - 'fun', - 'activity', - 'stringed', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'fish', - 'fishing pole', - 'fishing rod' - ]), - Emoji( - name: 'diving mask', - char: '\u{1F93F}', - shortName: 'diving_mask', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'uc12', - 'sport', - 'glasses', - 'vacation', - 'swim', - 'scuba', - 'fun', - 'activity', - 'mask', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'eyeglasses', - 'eye glasses', - 'swimming', - 'swimmer', - 'snorkel' - ]), - Emoji( - name: 'boxing glove', - char: '\u{1F94A}', - shortName: 'boxing_glove', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'boxing', - 'glove', - 'uc9', - 'sport', - 'fight', - 'gloves', - 'hit', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'punch', - 'pow', - 'bam' - ]), - Emoji( - name: 'martial arts uniform', - char: '\u{1F94B}', - shortName: 'martial_arts_uniform', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'judo', - 'karate', - 'martial arts', - 'taekwondo', - 'uniform', - 'uc9', - 'sport', - 'fight', - 'karate', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout' - ]), - Emoji( - name: 'running shirt', - char: '\u{1F3BD}', - shortName: 'running_shirt_with_sash', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'athletics', - 'running', - 'sash', - 'shirt', - 'uc6', - 'sport', - 'award', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero' - ]), - Emoji( - name: 'skateboard', - char: '\u{1F6F9}', - shortName: 'skateboard', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'uc11', - 'sport', - 'fun', - 'activity', - 'boosted', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'boosted board' - ]), - Emoji( - name: 'roller skate', - char: '\u{1F6FC}', - shortName: 'roller_skate', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'uc13', - 'sport', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout' - ]), - Emoji( - name: 'sled', - char: '\u{1F6F7}', - shortName: 'sled', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'uc10', - 'sport', - 'winter', - 'christmas', - 'fun', - 'activity', - 'sleigh', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'sledge', - 'toboggan' - ]), - Emoji( - name: 'ice skate', - char: '\u{26F8}\u{FE0F}', - shortName: 'ice_skate', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'ice', - 'skate', - 'uc5', - 'sport', - 'winter', - 'cold', - 'ice skating', - 'disney', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles', - 'cartoon' - ]), - Emoji( - name: 'curling stone', - char: '\u{1F94C}', - shortName: 'curling_stone', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'game', - 'rock', - 'uc10', - 'sport', - 'winter', - 'game', - 'play', - 'activity', - 'iron', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'games', - 'gaming' - ]), - Emoji( - name: 'skis', - char: '\u{1F3BF}', - shortName: 'ski', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'ski', - 'snow', - 'uc6', - 'sport', - 'winter', - 'cold', - 'skiing', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles', - 'ski', - 'snow skiing', - 'ski boot' - ]), - Emoji( - name: 'skier', - char: '\u{26F7}\u{FE0F}', - shortName: 'skier', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ski', - 'snow', - 'uc5', - 'sport', - 'winter', - 'vacation', - 'cold', - 'skiing', - 'paris', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles', - 'ski', - 'snow skiing', - 'ski boot', - 'french', - 'france' - ]), - Emoji( - name: 'snowboarder', - char: '\u{1F3C2}', - shortName: 'snowboarder', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ski', - 'snow', - 'snowboard', - 'uc6', - 'sport', - 'diversity', - 'winter', - 'vacation', - 'cold', - 'snowboarding', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles', - 'snowboarder' - ]), - Emoji( - name: 'snowboarder: light skin tone', - char: '\u{1F3C2}\u{1F3FB}', - shortName: 'snowboarder_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'light skin tone', - 'ski', - 'snow', - 'snowboard', - 'uc8', - 'sport', - 'diversity', - 'winter', - 'vacation', - 'cold', - 'snowboarding', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles', - 'snowboarder' - ], - modifiable: true), - Emoji( - name: 'snowboarder: medium-light skin tone', - char: '\u{1F3C2}\u{1F3FC}', - shortName: 'snowboarder_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'medium-light skin tone', - 'ski', - 'snow', - 'snowboard', - 'uc8', - 'sport', - 'diversity', - 'winter', - 'vacation', - 'cold', - 'snowboarding', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles', - 'snowboarder' - ], - modifiable: true), - Emoji( - name: 'snowboarder: medium skin tone', - char: '\u{1F3C2}\u{1F3FD}', - shortName: 'snowboarder_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'medium skin tone', - 'ski', - 'snow', - 'snowboard', - 'uc8', - 'sport', - 'diversity', - 'winter', - 'vacation', - 'cold', - 'snowboarding', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles', - 'snowboarder' - ], - modifiable: true), - Emoji( - name: 'snowboarder: medium-dark skin tone', - char: '\u{1F3C2}\u{1F3FE}', - shortName: 'snowboarder_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'medium-dark skin tone', - 'ski', - 'snow', - 'snowboard', - 'uc8', - 'sport', - 'diversity', - 'winter', - 'vacation', - 'cold', - 'snowboarding', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles', - 'snowboarder' - ], - modifiable: true), - Emoji( - name: 'snowboarder: dark skin tone', - char: '\u{1F3C2}\u{1F3FF}', - shortName: 'snowboarder_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'dark skin tone', - 'ski', - 'snow', - 'snowboard', - 'uc8', - 'sport', - 'diversity', - 'winter', - 'vacation', - 'cold', - 'snowboarding', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles', - 'snowboarder' - ], - modifiable: true), - Emoji( - name: 'parachute', - char: '\u{1FA82}', - shortName: 'parachute', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportAir, - keywords: [ - 'uc12', - 'sport', - 'fly', - 'vacation', - 'airplane', - 'fun', - 'activity', - 'kite', - 'skydive', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'flight', - 'flying', - 'flights', - 'avion', - 'airline', - 'aircraft', - 'airforce', - 'air force', - 'airport', - 'hang-glide', - 'parasail' - ]), - Emoji( - name: 'person lifting weights', - char: '\u{1F3CB}', - shortName: 'person_lifting_weights', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'lifter', - 'weight', - 'uc7', - 'sport', - 'diversity', - 'flex', - 'weight lifting', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'strong', - 'weight lifter' - ]), - Emoji( - name: 'person lifting weights: light skin tone', - char: '\u{1F3CB}\u{1F3FB}', - shortName: 'person_lifting_weights_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'lifter', - 'light skin tone', - 'weight', - 'uc8', - 'sport', - 'diversity', - 'flex', - 'weight lifting', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'strong', - 'weight lifter' - ], - modifiable: true), - Emoji( - name: 'person lifting weights: medium-light skin tone', - char: '\u{1F3CB}\u{1F3FC}', - shortName: 'person_lifting_weights_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'lifter', - 'medium-light skin tone', - 'weight', - 'uc8', - 'sport', - 'diversity', - 'flex', - 'weight lifting', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'strong', - 'weight lifter' - ], - modifiable: true), - Emoji( - name: 'person lifting weights: medium skin tone', - char: '\u{1F3CB}\u{1F3FD}', - shortName: 'person_lifting_weights_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'lifter', - 'medium skin tone', - 'weight', - 'uc8', - 'sport', - 'diversity', - 'flex', - 'weight lifting', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'strong', - 'weight lifter' - ], - modifiable: true), - Emoji( - name: 'person lifting weights: medium-dark skin tone', - char: '\u{1F3CB}\u{1F3FE}', - shortName: 'person_lifting_weights_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'lifter', - 'medium-dark skin tone', - 'weight', - 'uc8', - 'sport', - 'diversity', - 'flex', - 'weight lifting', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'strong', - 'weight lifter' - ], - modifiable: true), - Emoji( - name: 'person lifting weights: dark skin tone', - char: '\u{1F3CB}\u{1F3FF}', - shortName: 'person_lifting_weights_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'dark skin tone', - 'lifter', - 'weight', - 'uc8', - 'sport', - 'diversity', - 'flex', - 'weight lifting', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'strong', - 'weight lifter' - ], - modifiable: true), - Emoji( - name: 'woman lifting weights', - char: '\u{1F3CB}\u{FE0F}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_lifting_weights', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'weight lifter', - 'woman', - 'uc7', - 'sport', - 'diversity', - 'flex', - 'weight lifting', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'strong', - 'weight lifter' - ]), - Emoji( - name: 'woman lifting weights: light skin tone', - char: '\u{1F3CB}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_lifting_weights_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'light skin tone', - 'weight lifter', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'flex', - 'weight lifting', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'strong', - 'weight lifter' - ], - modifiable: true), - Emoji( - name: 'woman lifting weights: medium-light skin tone', - char: '\u{1F3CB}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_lifting_weights_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'medium-light skin tone', - 'weight lifter', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'flex', - 'weight lifting', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'strong', - 'weight lifter' - ], - modifiable: true), - Emoji( - name: 'woman lifting weights: medium skin tone', - char: '\u{1F3CB}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_lifting_weights_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'medium skin tone', - 'weight lifter', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'flex', - 'weight lifting', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'strong', - 'weight lifter' - ], - modifiable: true), - Emoji( - name: 'woman lifting weights: medium-dark skin tone', - char: '\u{1F3CB}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_lifting_weights_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'medium-dark skin tone', - 'weight lifter', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'flex', - 'weight lifting', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'strong', - 'weight lifter' - ], - modifiable: true), - Emoji( - name: 'woman lifting weights: dark skin tone', - char: '\u{1F3CB}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_lifting_weights_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'dark skin tone', - 'weight lifter', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'flex', - 'weight lifting', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'strong', - 'weight lifter' - ], - modifiable: true), - Emoji( - name: 'man lifting weights', - char: '\u{1F3CB}\u{FE0F}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_lifting_weights', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'man', - 'weight lifter', - 'uc7', - 'sport', - 'diversity', - 'flex', - 'weight lifting', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'strong', - 'weight lifter' - ]), - Emoji( - name: 'man lifting weights: light skin tone', - char: '\u{1F3CB}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_lifting_weights_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'light skin tone', - 'man', - 'weight lifter', - 'uc8', - 'sport', - 'diversity', - 'flex', - 'weight lifting', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'strong', - 'weight lifter' - ], - modifiable: true), - Emoji( - name: 'man lifting weights: medium-light skin tone', - char: '\u{1F3CB}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_lifting_weights_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'man', - 'medium-light skin tone', - 'weight lifter', - 'uc8', - 'sport', - 'diversity', - 'flex', - 'weight lifting', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'strong', - 'weight lifter' - ], - modifiable: true), - Emoji( - name: 'man lifting weights: medium skin tone', - char: '\u{1F3CB}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_lifting_weights_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'man', - 'medium skin tone', - 'weight lifter', - 'uc8', - 'sport', - 'diversity', - 'flex', - 'weight lifting', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'strong', - 'weight lifter' - ], - modifiable: true), - Emoji( - name: 'man lifting weights: medium-dark skin tone', - char: '\u{1F3CB}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_lifting_weights_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'man', - 'medium-dark skin tone', - 'weight lifter', - 'uc8', - 'sport', - 'diversity', - 'flex', - 'weight lifting', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'strong', - 'weight lifter' - ], - modifiable: true), - Emoji( - name: 'man lifting weights: dark skin tone', - char: '\u{1F3CB}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_lifting_weights_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'dark skin tone', - 'man', - 'weight lifter', - 'uc8', - 'sport', - 'diversity', - 'flex', - 'weight lifting', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'strong', - 'weight lifter' - ], - modifiable: true), - Emoji( - name: 'people wrestling', - char: '\u{1F93C}', - shortName: 'people_wrestling', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'wrestle', - 'wrestler', - 'uc9', - 'sport', - 'fight', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout' - ]), - Emoji( - name: 'women wrestling', - char: '\u{1F93C}\u{200D}\u{2640}\u{FE0F}', - shortName: 'women_wrestling', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'women', - 'wrestle', - 'uc9', - 'sport', - 'fight', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout' - ]), - Emoji( - name: 'men wrestling', - char: '\u{1F93C}\u{200D}\u{2642}\u{FE0F}', - shortName: 'men_wrestling', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'men', - 'wrestle', - 'uc9', - 'sport', - 'fight', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout' - ]), - Emoji( - name: 'person cartwheeling', - char: '\u{1F938}', - shortName: 'person_doing_cartwheel', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'cartwheel', - 'gymnastics', - 'uc9', - 'sport', - 'circus', - 'gymnast', - 'yoga', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'circus tent', - 'clown', - 'clowns', - 'gymnastics', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum' - ]), - Emoji( - name: 'person cartwheeling: light skin tone', - char: '\u{1F938}\u{1F3FB}', - shortName: 'person_doing_cartwheel_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'cartwheel', - 'gymnastics', - 'light skin tone', - 'uc9', - 'sport', - 'circus', - 'gymnast', - 'yoga', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'circus tent', - 'clown', - 'clowns', - 'gymnastics', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum' - ], - modifiable: true), - Emoji( - name: 'person cartwheeling: medium-light skin tone', - char: '\u{1F938}\u{1F3FC}', - shortName: 'person_doing_cartwheel_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'cartwheel', - 'gymnastics', - 'medium-light skin tone', - 'uc9', - 'sport', - 'circus', - 'gymnast', - 'yoga', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'circus tent', - 'clown', - 'clowns', - 'gymnastics', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum' - ], - modifiable: true), - Emoji( - name: 'person cartwheeling: medium skin tone', - char: '\u{1F938}\u{1F3FD}', - shortName: 'person_doing_cartwheel_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'cartwheel', - 'gymnastics', - 'medium skin tone', - 'uc9', - 'sport', - 'circus', - 'gymnast', - 'yoga', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'circus tent', - 'clown', - 'clowns', - 'gymnastics', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum' - ], - modifiable: true), - Emoji( - name: 'person cartwheeling: medium-dark skin tone', - char: '\u{1F938}\u{1F3FE}', - shortName: 'person_doing_cartwheel_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'cartwheel', - 'gymnastics', - 'medium-dark skin tone', - 'uc9', - 'sport', - 'circus', - 'gymnast', - 'yoga', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'circus tent', - 'clown', - 'clowns', - 'gymnastics', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum' - ], - modifiable: true), - Emoji( - name: 'person cartwheeling: dark skin tone', - char: '\u{1F938}\u{1F3FF}', - shortName: 'person_doing_cartwheel_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'cartwheel', - 'dark skin tone', - 'gymnastics', - 'uc9', - 'sport', - 'circus', - 'gymnast', - 'yoga', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'circus tent', - 'clown', - 'clowns', - 'gymnastics', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum' - ], - modifiable: true), - Emoji( - name: 'woman cartwheeling', - char: '\u{1F938}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_cartwheeling', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'cartwheel', - 'gymnastics', - 'woman', - 'uc9', - 'sport', - 'diversity', - 'circus', - 'gymnast', - 'yoga', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'circus tent', - 'clown', - 'clowns', - 'gymnastics', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum' - ]), - Emoji( - name: 'woman cartwheeling: light skin tone', - char: '\u{1F938}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_cartwheeling_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'cartwheel', - 'gymnastics', - 'light skin tone', - 'woman', - 'uc9', - 'sport', - 'diversity', - 'circus', - 'gymnast', - 'yoga', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'circus tent', - 'clown', - 'clowns', - 'gymnastics', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum' - ], - modifiable: true), - Emoji( - name: 'woman cartwheeling: medium-light skin tone', - char: '\u{1F938}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_cartwheeling_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'cartwheel', - 'gymnastics', - 'medium-light skin tone', - 'woman', - 'uc9', - 'sport', - 'diversity', - 'circus', - 'gymnast', - 'yoga', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'circus tent', - 'clown', - 'clowns', - 'gymnastics', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum' - ], - modifiable: true), - Emoji( - name: 'woman cartwheeling: medium skin tone', - char: '\u{1F938}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_cartwheeling_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'cartwheel', - 'gymnastics', - 'medium skin tone', - 'woman', - 'uc9', - 'sport', - 'diversity', - 'circus', - 'gymnast', - 'yoga', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'circus tent', - 'clown', - 'clowns', - 'gymnastics', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum' - ], - modifiable: true), - Emoji( - name: 'woman cartwheeling: medium-dark skin tone', - char: '\u{1F938}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_cartwheeling_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'cartwheel', - 'gymnastics', - 'medium-dark skin tone', - 'woman', - 'uc9', - 'sport', - 'diversity', - 'circus', - 'gymnast', - 'yoga', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'circus tent', - 'clown', - 'clowns', - 'gymnastics', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum' - ], - modifiable: true), - Emoji( - name: 'woman cartwheeling: dark skin tone', - char: '\u{1F938}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_cartwheeling_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'cartwheel', - 'dark skin tone', - 'gymnastics', - 'woman', - 'uc9', - 'sport', - 'diversity', - 'circus', - 'gymnast', - 'yoga', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'circus tent', - 'clown', - 'clowns', - 'gymnastics', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum' - ], - modifiable: true), - Emoji( - name: 'man cartwheeling', - char: '\u{1F938}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_cartwheeling', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'cartwheel', - 'gymnastics', - 'man', - 'uc9', - 'sport', - 'diversity', - 'circus', - 'gymnast', - 'yoga', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'circus tent', - 'clown', - 'clowns', - 'gymnastics', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum' - ]), - Emoji( - name: 'man cartwheeling: light skin tone', - char: '\u{1F938}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_cartwheeling_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'cartwheel', - 'gymnastics', - 'light skin tone', - 'man', - 'uc9', - 'sport', - 'diversity', - 'circus', - 'gymnast', - 'yoga', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'circus tent', - 'clown', - 'clowns', - 'gymnastics', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum' - ], - modifiable: true), - Emoji( - name: 'man cartwheeling: medium-light skin tone', - char: '\u{1F938}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_cartwheeling_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'cartwheel', - 'gymnastics', - 'man', - 'medium-light skin tone', - 'uc9', - 'sport', - 'diversity', - 'circus', - 'gymnast', - 'yoga', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'circus tent', - 'clown', - 'clowns', - 'gymnastics', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum' - ], - modifiable: true), - Emoji( - name: 'man cartwheeling: medium skin tone', - char: '\u{1F938}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_cartwheeling_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'cartwheel', - 'gymnastics', - 'man', - 'medium skin tone', - 'uc9', - 'sport', - 'diversity', - 'circus', - 'gymnast', - 'yoga', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'circus tent', - 'clown', - 'clowns', - 'gymnastics', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum' - ], - modifiable: true), - Emoji( - name: 'man cartwheeling: medium-dark skin tone', - char: '\u{1F938}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_cartwheeling_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'cartwheel', - 'gymnastics', - 'man', - 'medium-dark skin tone', - 'uc9', - 'sport', - 'diversity', - 'circus', - 'gymnast', - 'yoga', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'circus tent', - 'clown', - 'clowns', - 'gymnastics', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum' - ], - modifiable: true), - Emoji( - name: 'man cartwheeling: dark skin tone', - char: '\u{1F938}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_cartwheeling_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'cartwheel', - 'dark skin tone', - 'gymnastics', - 'man', - 'uc9', - 'sport', - 'diversity', - 'circus', - 'gymnast', - 'yoga', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'circus tent', - 'clown', - 'clowns', - 'gymnastics', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum' - ], - modifiable: true), - Emoji( - name: 'person bouncing ball', - char: '\u{26F9}', - shortName: 'person_bouncing_ball', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'uc5', - 'sport', - 'diversity', - 'ball', - 'basketball', - 'play', - 'fame', - 'fun', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'famous', - 'celebrity' - ]), - Emoji( - name: 'person bouncing ball: light skin tone', - char: '\u{26F9}\u{1F3FB}', - shortName: 'person_bouncing_ball_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'light skin tone', - 'uc8', - 'sport', - 'diversity', - 'ball', - 'basketball', - 'play', - 'fame', - 'fun', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'person bouncing ball: medium-light skin tone', - char: '\u{26F9}\u{1F3FC}', - shortName: 'person_bouncing_ball_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'medium-light skin tone', - 'uc8', - 'sport', - 'diversity', - 'ball', - 'basketball', - 'play', - 'fame', - 'fun', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'person bouncing ball: medium skin tone', - char: '\u{26F9}\u{1F3FD}', - shortName: 'person_bouncing_ball_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'medium skin tone', - 'uc8', - 'sport', - 'diversity', - 'ball', - 'basketball', - 'play', - 'fame', - 'fun', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'person bouncing ball: medium-dark skin tone', - char: '\u{26F9}\u{1F3FE}', - shortName: 'person_bouncing_ball_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'medium-dark skin tone', - 'uc8', - 'sport', - 'diversity', - 'ball', - 'basketball', - 'play', - 'fame', - 'fun', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'person bouncing ball: dark skin tone', - char: '\u{26F9}\u{1F3FF}', - shortName: 'person_bouncing_ball_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'dark skin tone', - 'uc8', - 'sport', - 'diversity', - 'ball', - 'basketball', - 'play', - 'fame', - 'fun', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'woman bouncing ball', - char: '\u{26F9}\u{FE0F}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_bouncing_ball', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'woman', - 'uc5', - 'sport', - 'diversity', - 'ball', - 'basketball', - 'play', - 'fame', - 'fun', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'famous', - 'celebrity' - ]), - Emoji( - name: 'woman bouncing ball: light skin tone', - char: '\u{26F9}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_bouncing_ball_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'light skin tone', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'ball', - 'basketball', - 'play', - 'fame', - 'fun', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'woman bouncing ball: medium-light skin tone', - char: '\u{26F9}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_bouncing_ball_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'medium-light skin tone', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'ball', - 'basketball', - 'play', - 'fame', - 'fun', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'woman bouncing ball: medium skin tone', - char: '\u{26F9}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_bouncing_ball_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'medium skin tone', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'ball', - 'basketball', - 'play', - 'fame', - 'fun', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'woman bouncing ball: medium-dark skin tone', - char: '\u{26F9}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_bouncing_ball_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'medium-dark skin tone', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'ball', - 'basketball', - 'play', - 'fame', - 'fun', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'woman bouncing ball: dark skin tone', - char: '\u{26F9}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_bouncing_ball_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'dark skin tone', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'ball', - 'basketball', - 'play', - 'fame', - 'fun', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'man bouncing ball', - char: '\u{26F9}\u{FE0F}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_bouncing_ball', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'man', - 'uc5', - 'sport', - 'ball', - 'basketball', - 'play', - 'fame', - 'fun', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'balls', - 'ballon', - 'famous', - 'celebrity' - ]), - Emoji( - name: 'man bouncing ball: light skin tone', - char: '\u{26F9}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_bouncing_ball_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'light skin tone', - 'man', - 'uc8', - 'sport', - 'ball', - 'basketball', - 'play', - 'fame', - 'fun', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'balls', - 'ballon', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'man bouncing ball: medium-light skin tone', - char: '\u{26F9}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_bouncing_ball_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'man', - 'medium-light skin tone', - 'uc8', - 'sport', - 'ball', - 'basketball', - 'play', - 'fame', - 'fun', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'balls', - 'ballon', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'man bouncing ball: medium skin tone', - char: '\u{26F9}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_bouncing_ball_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'man', - 'medium skin tone', - 'uc8', - 'sport', - 'ball', - 'basketball', - 'play', - 'fame', - 'fun', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'balls', - 'ballon', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'man bouncing ball: medium-dark skin tone', - char: '\u{26F9}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_bouncing_ball_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'man', - 'medium-dark skin tone', - 'uc8', - 'sport', - 'ball', - 'basketball', - 'play', - 'fame', - 'fun', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'balls', - 'ballon', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'man bouncing ball: dark skin tone', - char: '\u{26F9}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_bouncing_ball_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'dark skin tone', - 'man', - 'uc8', - 'sport', - 'ball', - 'basketball', - 'play', - 'fame', - 'fun', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'balls', - 'ballon', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'person fencing', - char: '\u{1F93A}', - shortName: 'person_fencing', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'fencer', - 'fencing', - 'sword', - 'uc9', - 'sport', - 'fight', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout' - ]), - Emoji( - name: 'person playing handball', - char: '\u{1F93E}', - shortName: 'person_playing_handball', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'handball', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'volley ball', - 'play', - 'throw', - 'jump', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ]), - Emoji( - name: 'person playing handball: light skin tone', - char: '\u{1F93E}\u{1F3FB}', - shortName: 'person_playing_handball_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'handball', - 'light skin tone', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'volley ball', - 'play', - 'throw', - 'jump', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'person playing handball: medium-light skin tone', - char: '\u{1F93E}\u{1F3FC}', - shortName: 'person_playing_handball_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'handball', - 'medium-light skin tone', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'volley ball', - 'play', - 'throw', - 'jump', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'person playing handball: medium skin tone', - char: '\u{1F93E}\u{1F3FD}', - shortName: 'person_playing_handball_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'handball', - 'medium skin tone', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'volley ball', - 'play', - 'throw', - 'jump', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'person playing handball: medium-dark skin tone', - char: '\u{1F93E}\u{1F3FE}', - shortName: 'person_playing_handball_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'handball', - 'medium-dark skin tone', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'volley ball', - 'play', - 'throw', - 'jump', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'person playing handball: dark skin tone', - char: '\u{1F93E}\u{1F3FF}', - shortName: 'person_playing_handball_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'dark skin tone', - 'handball', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'volley ball', - 'play', - 'throw', - 'jump', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'woman playing handball', - char: '\u{1F93E}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_playing_handball', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'handball', - 'woman', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'volley ball', - 'play', - 'throw', - 'jump', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ]), - Emoji( - name: 'woman playing handball: light skin tone', - char: '\u{1F93E}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_playing_handball_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'handball', - 'light skin tone', - 'woman', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'volley ball', - 'play', - 'throw', - 'jump', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'woman playing handball: medium-light skin tone', - char: '\u{1F93E}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_playing_handball_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'handball', - 'medium-light skin tone', - 'woman', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'volley ball', - 'play', - 'throw', - 'jump', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'woman playing handball: medium skin tone', - char: '\u{1F93E}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_playing_handball_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'handball', - 'medium skin tone', - 'woman', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'volley ball', - 'play', - 'throw', - 'jump', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'woman playing handball: medium-dark skin tone', - char: '\u{1F93E}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_playing_handball_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'handball', - 'medium-dark skin tone', - 'woman', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'volley ball', - 'play', - 'throw', - 'jump', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'woman playing handball: dark skin tone', - char: '\u{1F93E}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_playing_handball_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'dark skin tone', - 'handball', - 'woman', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'volley ball', - 'play', - 'throw', - 'jump', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'man playing handball', - char: '\u{1F93E}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_playing_handball', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'handball', - 'man', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'volley ball', - 'play', - 'throw', - 'jump', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ]), - Emoji( - name: 'man playing handball: light skin tone', - char: '\u{1F93E}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_playing_handball_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'handball', - 'light skin tone', - 'man', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'volley ball', - 'play', - 'throw', - 'jump', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'man playing handball: medium-light skin tone', - char: '\u{1F93E}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_playing_handball_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'handball', - 'man', - 'medium-light skin tone', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'volley ball', - 'play', - 'throw', - 'jump', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'man playing handball: medium skin tone', - char: '\u{1F93E}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_playing_handball_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'handball', - 'man', - 'medium skin tone', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'volley ball', - 'play', - 'throw', - 'jump', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'man playing handball: medium-dark skin tone', - char: '\u{1F93E}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_playing_handball_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'handball', - 'man', - 'medium-dark skin tone', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'volley ball', - 'play', - 'throw', - 'jump', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'man playing handball: dark skin tone', - char: '\u{1F93E}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_playing_handball_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'dark skin tone', - 'handball', - 'man', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'volley ball', - 'play', - 'throw', - 'jump', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'person golfing', - char: '\u{1F3CC}', - shortName: 'person_golfing', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'golf', - 'uc7', - 'sport', - 'diversity', - 'ball', - 'vacation', - 'golf', - 'play', - 'florida', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'golfing', - 'golfer' - ]), - Emoji( - name: 'person golfing: light skin tone', - char: '\u{1F3CC}\u{1F3FB}', - shortName: 'person_golfing_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'golf', - 'light skin tone', - 'uc8', - 'sport', - 'diversity', - 'ball', - 'vacation', - 'golf', - 'play', - 'florida', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'golfing', - 'golfer' - ], - modifiable: true), - Emoji( - name: 'person golfing: medium-light skin tone', - char: '\u{1F3CC}\u{1F3FC}', - shortName: 'person_golfing_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'golf', - 'medium-light skin tone', - 'uc8', - 'sport', - 'diversity', - 'ball', - 'vacation', - 'golf', - 'play', - 'florida', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'golfing', - 'golfer' - ], - modifiable: true), - Emoji( - name: 'person golfing: medium skin tone', - char: '\u{1F3CC}\u{1F3FD}', - shortName: 'person_golfing_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'golf', - 'medium skin tone', - 'uc8', - 'sport', - 'diversity', - 'ball', - 'vacation', - 'golf', - 'play', - 'florida', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'golfing', - 'golfer' - ], - modifiable: true), - Emoji( - name: 'person golfing: medium-dark skin tone', - char: '\u{1F3CC}\u{1F3FE}', - shortName: 'person_golfing_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'golf', - 'medium-dark skin tone', - 'uc8', - 'sport', - 'diversity', - 'ball', - 'vacation', - 'golf', - 'play', - 'florida', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'golfing', - 'golfer' - ], - modifiable: true), - Emoji( - name: 'person golfing: dark skin tone', - char: '\u{1F3CC}\u{1F3FF}', - shortName: 'person_golfing_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'ball', - 'dark skin tone', - 'golf', - 'uc8', - 'sport', - 'diversity', - 'ball', - 'vacation', - 'golf', - 'play', - 'florida', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'golfing', - 'golfer' - ], - modifiable: true), - Emoji( - name: 'woman golfing', - char: '\u{1F3CC}\u{FE0F}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_golfing', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'golf', - 'woman', - 'uc7', - 'sport', - 'diversity', - 'ball', - 'vacation', - 'golf', - 'play', - 'florida', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'golfing', - 'golfer' - ]), - Emoji( - name: 'woman golfing: light skin tone', - char: '\u{1F3CC}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_golfing_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'golf', - 'light skin tone', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'ball', - 'vacation', - 'golf', - 'play', - 'florida', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'golfing', - 'golfer' - ], - modifiable: true), - Emoji( - name: 'woman golfing: medium-light skin tone', - char: '\u{1F3CC}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_golfing_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'golf', - 'medium-light skin tone', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'ball', - 'vacation', - 'golf', - 'play', - 'florida', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'golfing', - 'golfer' - ], - modifiable: true), - Emoji( - name: 'woman golfing: medium skin tone', - char: '\u{1F3CC}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_golfing_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'golf', - 'medium skin tone', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'ball', - 'vacation', - 'golf', - 'play', - 'florida', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'golfing', - 'golfer' - ], - modifiable: true), - Emoji( - name: 'woman golfing: medium-dark skin tone', - char: '\u{1F3CC}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_golfing_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'golf', - 'medium-dark skin tone', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'ball', - 'vacation', - 'golf', - 'play', - 'florida', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'golfing', - 'golfer' - ], - modifiable: true), - Emoji( - name: 'woman golfing: dark skin tone', - char: '\u{1F3CC}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_golfing_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'dark skin tone', - 'golf', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'ball', - 'vacation', - 'golf', - 'play', - 'florida', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'golfing', - 'golfer' - ], - modifiable: true), - Emoji( - name: 'man golfing', - char: '\u{1F3CC}\u{FE0F}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_golfing', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'golf', - 'man', - 'uc7', - 'sport', - 'diversity', - 'ball', - 'vacation', - 'golf', - 'play', - 'florida', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'golfing', - 'golfer' - ]), - Emoji( - name: 'man golfing: light skin tone', - char: '\u{1F3CC}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_golfing_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'golf', - 'light skin tone', - 'man', - 'uc8', - 'sport', - 'diversity', - 'ball', - 'vacation', - 'golf', - 'play', - 'florida', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'golfing', - 'golfer' - ], - modifiable: true), - Emoji( - name: 'man golfing: medium-light skin tone', - char: '\u{1F3CC}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_golfing_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'golf', - 'man', - 'medium-light skin tone', - 'uc8', - 'sport', - 'diversity', - 'ball', - 'vacation', - 'golf', - 'play', - 'florida', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'golfing', - 'golfer' - ], - modifiable: true), - Emoji( - name: 'man golfing: medium skin tone', - char: '\u{1F3CC}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_golfing_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'golf', - 'man', - 'medium skin tone', - 'uc8', - 'sport', - 'diversity', - 'ball', - 'vacation', - 'golf', - 'play', - 'florida', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'golfing', - 'golfer' - ], - modifiable: true), - Emoji( - name: 'man golfing: medium-dark skin tone', - char: '\u{1F3CC}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_golfing_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'golf', - 'man', - 'medium-dark skin tone', - 'uc8', - 'sport', - 'diversity', - 'ball', - 'vacation', - 'golf', - 'play', - 'florida', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'golfing', - 'golfer' - ], - modifiable: true), - Emoji( - name: 'man golfing: dark skin tone', - char: '\u{1F3CC}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_golfing_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'dark skin tone', - 'golf', - 'man', - 'uc8', - 'sport', - 'diversity', - 'ball', - 'vacation', - 'golf', - 'play', - 'florida', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'golfing', - 'golfer' - ], - modifiable: true), - Emoji( - name: 'horse racing', - char: '\u{1F3C7}', - shortName: 'horse_racing', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'horse', - 'jockey', - 'racehorse', - 'racing', - 'uc6', - 'sport', - 'diversity', - 'horse racing', - 'las vegas', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'horseback riding', - 'horse and rider', - 'horses', - 'horseshoe', - 'pony', - 'vegas' - ]), - Emoji( - name: 'horse racing: light skin tone', - char: '\u{1F3C7}\u{1F3FB}', - shortName: 'horse_racing_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'horse', - 'jockey', - 'light skin tone', - 'racehorse', - 'racing', - 'uc8', - 'sport', - 'diversity', - 'horse racing', - 'las vegas', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'horseback riding', - 'horse and rider', - 'horses', - 'horseshoe', - 'pony', - 'vegas' - ], - modifiable: true), - Emoji( - name: 'horse racing: medium-light skin tone', - char: '\u{1F3C7}\u{1F3FC}', - shortName: 'horse_racing_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'horse', - 'jockey', - 'medium-light skin tone', - 'racehorse', - 'racing', - 'uc8', - 'sport', - 'diversity', - 'horse racing', - 'las vegas', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'horseback riding', - 'horse and rider', - 'horses', - 'horseshoe', - 'pony', - 'vegas' - ], - modifiable: true), - Emoji( - name: 'horse racing: medium skin tone', - char: '\u{1F3C7}\u{1F3FD}', - shortName: 'horse_racing_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'horse', - 'jockey', - 'medium skin tone', - 'racehorse', - 'racing', - 'uc8', - 'sport', - 'diversity', - 'horse racing', - 'las vegas', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'horseback riding', - 'horse and rider', - 'horses', - 'horseshoe', - 'pony', - 'vegas' - ], - modifiable: true), - Emoji( - name: 'horse racing: medium-dark skin tone', - char: '\u{1F3C7}\u{1F3FE}', - shortName: 'horse_racing_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'horse', - 'jockey', - 'medium-dark skin tone', - 'racehorse', - 'racing', - 'uc8', - 'sport', - 'diversity', - 'horse racing', - 'las vegas', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'horseback riding', - 'horse and rider', - 'horses', - 'horseshoe', - 'pony', - 'vegas' - ], - modifiable: true), - Emoji( - name: 'horse racing: dark skin tone', - char: '\u{1F3C7}\u{1F3FF}', - shortName: 'horse_racing_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'dark skin tone', - 'horse', - 'jockey', - 'racehorse', - 'racing', - 'uc8', - 'sport', - 'diversity', - 'horse racing', - 'las vegas', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'horseback riding', - 'horse and rider', - 'horses', - 'horseshoe', - 'pony', - 'vegas' - ], - modifiable: true), - Emoji( - name: 'person in lotus position', - char: '\u{1F9D8}', - shortName: 'person_in_lotus_position', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'uc10', - 'sport', - 'diversity', - 'vacation', - 'yoga', - 'california', - 'activity', - 'spa', - 'sit', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'sitting', - 'kneel', - 'kneeling' - ]), - Emoji( - name: 'person in lotus position: light skin tone', - char: '\u{1F9D8}\u{1F3FB}', - shortName: 'person_in_lotus_position_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'light skin tone', - 'meditation', - 'yoga', - 'uc10', - 'sport', - 'diversity', - 'vacation', - 'yoga', - 'california', - 'activity', - 'spa', - 'sit', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'person in lotus position: medium-light skin tone', - char: '\u{1F9D8}\u{1F3FC}', - shortName: 'person_in_lotus_position_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'meditation', - 'medium-light skin tone', - 'yoga', - 'uc10', - 'sport', - 'diversity', - 'vacation', - 'yoga', - 'california', - 'activity', - 'spa', - 'sit', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'person in lotus position: medium skin tone', - char: '\u{1F9D8}\u{1F3FD}', - shortName: 'person_in_lotus_position_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'meditation', - 'medium skin tone', - 'yoga', - 'uc10', - 'sport', - 'diversity', - 'vacation', - 'yoga', - 'california', - 'activity', - 'spa', - 'sit', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'person in lotus position: medium-dark skin tone', - char: '\u{1F9D8}\u{1F3FE}', - shortName: 'person_in_lotus_position_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'meditation', - 'medium-dark skin tone', - 'yoga', - 'uc10', - 'sport', - 'diversity', - 'vacation', - 'yoga', - 'california', - 'activity', - 'spa', - 'sit', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'person in lotus position: dark skin tone', - char: '\u{1F9D8}\u{1F3FF}', - shortName: 'person_in_lotus_position_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'dark skin tone', - 'meditation', - 'yoga', - 'uc10', - 'sport', - 'diversity', - 'vacation', - 'yoga', - 'california', - 'activity', - 'spa', - 'sit', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'woman in lotus position', - char: '\u{1F9D8}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_in_lotus_position', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'meditation', - 'yoga', - 'uc10', - 'sport', - 'diversity', - 'women', - 'vacation', - 'yoga', - 'california', - 'activity', - 'spa', - 'sit', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'sitting', - 'kneel', - 'kneeling' - ]), - Emoji( - name: 'woman in lotus position: light skin tone', - char: '\u{1F9D8}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_in_lotus_position_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'light skin tone', - 'meditation', - 'yoga', - 'uc10', - 'sport', - 'diversity', - 'women', - 'vacation', - 'yoga', - 'california', - 'activity', - 'spa', - 'sit', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'woman in lotus position: medium-light skin tone', - char: '\u{1F9D8}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_in_lotus_position_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'meditation', - 'medium-light skin tone', - 'yoga', - 'uc10', - 'sport', - 'diversity', - 'women', - 'vacation', - 'yoga', - 'california', - 'activity', - 'spa', - 'sit', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'woman in lotus position: medium skin tone', - char: '\u{1F9D8}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_in_lotus_position_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'meditation', - 'medium skin tone', - 'yoga', - 'uc10', - 'sport', - 'diversity', - 'women', - 'vacation', - 'yoga', - 'california', - 'activity', - 'spa', - 'sit', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'woman in lotus position: medium-dark skin tone', - char: '\u{1F9D8}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_in_lotus_position_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'meditation', - 'medium-dark skin tone', - 'yoga', - 'uc10', - 'sport', - 'diversity', - 'women', - 'vacation', - 'yoga', - 'california', - 'activity', - 'spa', - 'sit', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'woman in lotus position: dark skin tone', - char: '\u{1F9D8}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_in_lotus_position_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'dark skin tone', - 'meditation', - 'yoga', - 'uc10', - 'sport', - 'diversity', - 'women', - 'vacation', - 'yoga', - 'california', - 'activity', - 'spa', - 'sit', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'woman', - 'female', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'man in lotus position', - char: '\u{1F9D8}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_in_lotus_position', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'meditation', - 'yoga', - 'uc10', - 'sport', - 'diversity', - 'vacation', - 'yoga', - 'california', - 'activity', - 'spa', - 'sit', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'sitting', - 'kneel', - 'kneeling' - ]), - Emoji( - name: 'man in lotus position: light skin tone', - char: '\u{1F9D8}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_in_lotus_position_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'light skin tone', - 'meditation', - 'yoga', - 'uc10', - 'sport', - 'diversity', - 'vacation', - 'yoga', - 'california', - 'activity', - 'spa', - 'sit', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'man in lotus position: medium-light skin tone', - char: '\u{1F9D8}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_in_lotus_position_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'meditation', - 'medium-light skin tone', - 'yoga', - 'uc10', - 'sport', - 'diversity', - 'vacation', - 'yoga', - 'california', - 'activity', - 'spa', - 'sit', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'man in lotus position: medium skin tone', - char: '\u{1F9D8}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_in_lotus_position_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'meditation', - 'medium skin tone', - 'yoga', - 'uc10', - 'sport', - 'diversity', - 'vacation', - 'yoga', - 'california', - 'activity', - 'spa', - 'sit', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'man in lotus position: medium-dark skin tone', - char: '\u{1F9D8}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_in_lotus_position_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'meditation', - 'medium-dark skin tone', - 'yoga', - 'uc10', - 'sport', - 'diversity', - 'vacation', - 'yoga', - 'california', - 'activity', - 'spa', - 'sit', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'man in lotus position: dark skin tone', - char: '\u{1F9D8}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_in_lotus_position_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'dark skin tone', - 'meditation', - 'yoga', - 'uc10', - 'sport', - 'diversity', - 'vacation', - 'yoga', - 'california', - 'activity', - 'spa', - 'sit', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'meditation', - 'meditate', - 'zen', - 'om', - 'aum', - 'relax', - 'sauna', - 'sitting', - 'kneel', - 'kneeling' - ], - modifiable: true), - Emoji( - name: 'person surfing', - char: '\u{1F3C4}', - shortName: 'person_surfing', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'surfing', - 'uc6', - 'sport', - 'diversity', - 'tropical', - 'vacation', - 'hawaii', - 'california', - 'florida', - 'fun', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'weekend' - ]), - Emoji( - name: 'person surfing: light skin tone', - char: '\u{1F3C4}\u{1F3FB}', - shortName: 'person_surfing_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'light skin tone', - 'surfing', - 'uc8', - 'sport', - 'diversity', - 'tropical', - 'vacation', - 'hawaii', - 'california', - 'florida', - 'fun', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'person surfing: medium-light skin tone', - char: '\u{1F3C4}\u{1F3FC}', - shortName: 'person_surfing_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'medium-light skin tone', - 'surfing', - 'uc8', - 'sport', - 'diversity', - 'tropical', - 'vacation', - 'hawaii', - 'california', - 'florida', - 'fun', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'person surfing: medium skin tone', - char: '\u{1F3C4}\u{1F3FD}', - shortName: 'person_surfing_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'medium skin tone', - 'surfing', - 'uc8', - 'sport', - 'diversity', - 'tropical', - 'vacation', - 'hawaii', - 'california', - 'florida', - 'fun', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'person surfing: medium-dark skin tone', - char: '\u{1F3C4}\u{1F3FE}', - shortName: 'person_surfing_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'medium-dark skin tone', - 'surfing', - 'uc8', - 'sport', - 'diversity', - 'tropical', - 'vacation', - 'hawaii', - 'california', - 'florida', - 'fun', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'person surfing: dark skin tone', - char: '\u{1F3C4}\u{1F3FF}', - shortName: 'person_surfing_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'dark skin tone', - 'surfing', - 'uc8', - 'sport', - 'diversity', - 'tropical', - 'vacation', - 'hawaii', - 'california', - 'florida', - 'fun', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'woman surfing', - char: '\u{1F3C4}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_surfing', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'surfing', - 'woman', - 'uc6', - 'sport', - 'diversity', - 'tropical', - 'vacation', - 'hawaii', - 'california', - 'florida', - 'fun', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'weekend' - ]), - Emoji( - name: 'woman surfing: light skin tone', - char: '\u{1F3C4}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_surfing_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'light skin tone', - 'surfing', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'tropical', - 'vacation', - 'hawaii', - 'california', - 'florida', - 'fun', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'woman surfing: medium-light skin tone', - char: '\u{1F3C4}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_surfing_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'medium-light skin tone', - 'surfing', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'tropical', - 'vacation', - 'hawaii', - 'california', - 'florida', - 'fun', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'woman surfing: medium skin tone', - char: '\u{1F3C4}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_surfing_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'medium skin tone', - 'surfing', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'tropical', - 'vacation', - 'hawaii', - 'california', - 'florida', - 'fun', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'woman surfing: medium-dark skin tone', - char: '\u{1F3C4}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_surfing_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'medium-dark skin tone', - 'surfing', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'tropical', - 'vacation', - 'hawaii', - 'california', - 'florida', - 'fun', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'woman surfing: dark skin tone', - char: '\u{1F3C4}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_surfing_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'dark skin tone', - 'surfing', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'tropical', - 'vacation', - 'hawaii', - 'california', - 'florida', - 'fun', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'man surfing', - char: '\u{1F3C4}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_surfing', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'man', - 'surfing', - 'uc6', - 'sport', - 'diversity', - 'tropical', - 'vacation', - 'hawaii', - 'california', - 'florida', - 'fun', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'weekend' - ]), - Emoji( - name: 'man surfing: light skin tone', - char: '\u{1F3C4}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_surfing_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'light skin tone', - 'man', - 'surfing', - 'uc8', - 'sport', - 'diversity', - 'tropical', - 'vacation', - 'hawaii', - 'california', - 'florida', - 'fun', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'man surfing: medium-light skin tone', - char: '\u{1F3C4}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_surfing_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'man', - 'medium-light skin tone', - 'surfing', - 'uc8', - 'sport', - 'diversity', - 'tropical', - 'vacation', - 'hawaii', - 'california', - 'florida', - 'fun', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'man surfing: medium skin tone', - char: '\u{1F3C4}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_surfing_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'man', - 'medium skin tone', - 'surfing', - 'uc8', - 'sport', - 'diversity', - 'tropical', - 'vacation', - 'hawaii', - 'california', - 'florida', - 'fun', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'man surfing: medium-dark skin tone', - char: '\u{1F3C4}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_surfing_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'man', - 'medium-dark skin tone', - 'surfing', - 'uc8', - 'sport', - 'diversity', - 'tropical', - 'vacation', - 'hawaii', - 'california', - 'florida', - 'fun', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'man surfing: dark skin tone', - char: '\u{1F3C4}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_surfing_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'dark skin tone', - 'man', - 'surfing', - 'uc8', - 'sport', - 'diversity', - 'tropical', - 'vacation', - 'hawaii', - 'california', - 'florida', - 'fun', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'person swimming', - char: '\u{1F3CA}', - shortName: 'person_swimming', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'swim', - 'uc6', - 'sport', - 'diversity', - 'vacation', - 'swim', - 'scuba', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'swimming', - 'swimmer', - 'snorkel', - 'weekend' - ]), - Emoji( - name: 'person swimming: light skin tone', - char: '\u{1F3CA}\u{1F3FB}', - shortName: 'person_swimming_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'light skin tone', - 'swim', - 'uc8', - 'sport', - 'diversity', - 'vacation', - 'swim', - 'scuba', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'swimming', - 'swimmer', - 'snorkel', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'person swimming: medium-light skin tone', - char: '\u{1F3CA}\u{1F3FC}', - shortName: 'person_swimming_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'medium-light skin tone', - 'swim', - 'uc8', - 'sport', - 'diversity', - 'vacation', - 'swim', - 'scuba', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'swimming', - 'swimmer', - 'snorkel', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'person swimming: medium skin tone', - char: '\u{1F3CA}\u{1F3FD}', - shortName: 'person_swimming_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'medium skin tone', - 'swim', - 'uc8', - 'sport', - 'diversity', - 'vacation', - 'swim', - 'scuba', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'swimming', - 'swimmer', - 'snorkel', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'person swimming: medium-dark skin tone', - char: '\u{1F3CA}\u{1F3FE}', - shortName: 'person_swimming_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'medium-dark skin tone', - 'swim', - 'uc8', - 'sport', - 'diversity', - 'vacation', - 'swim', - 'scuba', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'swimming', - 'swimmer', - 'snorkel', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'person swimming: dark skin tone', - char: '\u{1F3CA}\u{1F3FF}', - shortName: 'person_swimming_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'dark skin tone', - 'swim', - 'uc8', - 'sport', - 'diversity', - 'vacation', - 'swim', - 'scuba', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'swimming', - 'swimmer', - 'snorkel', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'woman swimming', - char: '\u{1F3CA}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_swimming', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'swim', - 'woman', - 'uc6', - 'sport', - 'diversity', - 'vacation', - 'swim', - 'scuba', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'swimming', - 'swimmer', - 'snorkel', - 'weekend' - ]), - Emoji( - name: 'woman swimming: light skin tone', - char: '\u{1F3CA}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_swimming_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'light skin tone', - 'swim', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'vacation', - 'swim', - 'scuba', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'swimming', - 'swimmer', - 'snorkel', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'woman swimming: medium-light skin tone', - char: '\u{1F3CA}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_swimming_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'medium-light skin tone', - 'swim', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'vacation', - 'swim', - 'scuba', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'swimming', - 'swimmer', - 'snorkel', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'woman swimming: medium skin tone', - char: '\u{1F3CA}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_swimming_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'medium skin tone', - 'swim', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'vacation', - 'swim', - 'scuba', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'swimming', - 'swimmer', - 'snorkel', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'woman swimming: medium-dark skin tone', - char: '\u{1F3CA}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_swimming_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'medium-dark skin tone', - 'swim', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'vacation', - 'swim', - 'scuba', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'swimming', - 'swimmer', - 'snorkel', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'woman swimming: dark skin tone', - char: '\u{1F3CA}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_swimming_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'dark skin tone', - 'swim', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'vacation', - 'swim', - 'scuba', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'swimming', - 'swimmer', - 'snorkel', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'man swimming', - char: '\u{1F3CA}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_swimming', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'man', - 'swim', - 'uc6', - 'sport', - 'diversity', - 'vacation', - 'swim', - 'scuba', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'swimming', - 'swimmer', - 'snorkel', - 'weekend' - ]), - Emoji( - name: 'man swimming: light skin tone', - char: '\u{1F3CA}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_swimming_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'light skin tone', - 'man', - 'swim', - 'uc8', - 'sport', - 'diversity', - 'vacation', - 'swim', - 'scuba', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'swimming', - 'swimmer', - 'snorkel', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'man swimming: medium-light skin tone', - char: '\u{1F3CA}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_swimming_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'man', - 'medium-light skin tone', - 'swim', - 'uc8', - 'sport', - 'diversity', - 'vacation', - 'swim', - 'scuba', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'swimming', - 'swimmer', - 'snorkel', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'man swimming: medium skin tone', - char: '\u{1F3CA}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_swimming_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'man', - 'medium skin tone', - 'swim', - 'uc8', - 'sport', - 'diversity', - 'vacation', - 'swim', - 'scuba', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'swimming', - 'swimmer', - 'snorkel', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'man swimming: medium-dark skin tone', - char: '\u{1F3CA}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_swimming_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'man', - 'medium-dark skin tone', - 'swim', - 'uc8', - 'sport', - 'diversity', - 'vacation', - 'swim', - 'scuba', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'swimming', - 'swimmer', - 'snorkel', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'man swimming: dark skin tone', - char: '\u{1F3CA}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_swimming_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'dark skin tone', - 'man', - 'swim', - 'uc8', - 'sport', - 'diversity', - 'vacation', - 'swim', - 'scuba', - 'summer', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'swimming', - 'swimmer', - 'snorkel', - 'weekend' - ], - modifiable: true), - Emoji( - name: 'person playing water polo', - char: '\u{1F93D}', - shortName: 'person_playing_water_polo', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'polo', - 'water', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'play', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ]), - Emoji( - name: 'person playing water polo: light skin tone', - char: '\u{1F93D}\u{1F3FB}', - shortName: 'person_playing_water_polo_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'light skin tone', - 'polo', - 'water', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'play', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'person playing water polo: medium-light skin tone', - char: '\u{1F93D}\u{1F3FC}', - shortName: 'person_playing_water_polo_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'medium-light skin tone', - 'polo', - 'water', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'play', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'person playing water polo: medium skin tone', - char: '\u{1F93D}\u{1F3FD}', - shortName: 'person_playing_water_polo_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'medium skin tone', - 'polo', - 'water', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'play', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'person playing water polo: medium-dark skin tone', - char: '\u{1F93D}\u{1F3FE}', - shortName: 'person_playing_water_polo_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'medium-dark skin tone', - 'polo', - 'water', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'play', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'person playing water polo: dark skin tone', - char: '\u{1F93D}\u{1F3FF}', - shortName: 'person_playing_water_polo_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'dark skin tone', - 'polo', - 'water', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'play', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'woman playing water polo', - char: '\u{1F93D}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_playing_water_polo', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'water polo', - 'woman', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'play', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ]), - Emoji( - name: 'woman playing water polo: light skin tone', - char: '\u{1F93D}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_playing_water_polo_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'light skin tone', - 'water polo', - 'woman', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'play', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'woman playing water polo: medium-light skin tone', - char: '\u{1F93D}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_playing_water_polo_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'medium-light skin tone', - 'water polo', - 'woman', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'play', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'woman playing water polo: medium skin tone', - char: '\u{1F93D}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_playing_water_polo_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'medium skin tone', - 'water polo', - 'woman', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'play', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'woman playing water polo: medium-dark skin tone', - char: '\u{1F93D}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_playing_water_polo_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'medium-dark skin tone', - 'water polo', - 'woman', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'play', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'woman playing water polo: dark skin tone', - char: '\u{1F93D}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_playing_water_polo_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'dark skin tone', - 'water polo', - 'woman', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'play', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'man playing water polo', - char: '\u{1F93D}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_playing_water_polo', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'man', - 'water polo', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'play', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ]), - Emoji( - name: 'man playing water polo: light skin tone', - char: '\u{1F93D}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_playing_water_polo_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'light skin tone', - 'man', - 'water polo', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'play', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'man playing water polo: medium-light skin tone', - char: '\u{1F93D}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_playing_water_polo_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'man', - 'medium-light skin tone', - 'water polo', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'play', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'man playing water polo: medium skin tone', - char: '\u{1F93D}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_playing_water_polo_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'man', - 'medium skin tone', - 'water polo', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'play', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'man playing water polo: medium-dark skin tone', - char: '\u{1F93D}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_playing_water_polo_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'man', - 'medium-dark skin tone', - 'water polo', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'play', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'man playing water polo: dark skin tone', - char: '\u{1F93D}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_playing_water_polo_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'dark skin tone', - 'man', - 'water polo', - 'uc9', - 'sport', - 'diversity', - 'ball', - 'play', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon' - ], - modifiable: true), - Emoji( - name: 'person rowing boat', - char: '\u{1F6A3}', - shortName: 'person_rowing_boat', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'boat', - 'rowboat', - 'uc6', - 'sport', - 'diversity', - 'boat', - 'rowing', - 'hawaii', - 'scuba', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'boats', - 'boating', - 'rowboat', - 'canoe', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'snorkel' - ]), - Emoji( - name: 'person rowing boat: light skin tone', - char: '\u{1F6A3}\u{1F3FB}', - shortName: 'person_rowing_boat_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'boat', - 'light skin tone', - 'rowboat', - 'uc8', - 'sport', - 'diversity', - 'boat', - 'rowing', - 'hawaii', - 'scuba', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'boats', - 'boating', - 'rowboat', - 'canoe', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'snorkel' - ], - modifiable: true), - Emoji( - name: 'person rowing boat: medium-light skin tone', - char: '\u{1F6A3}\u{1F3FC}', - shortName: 'person_rowing_boat_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'boat', - 'medium-light skin tone', - 'rowboat', - 'uc8', - 'sport', - 'diversity', - 'boat', - 'rowing', - 'hawaii', - 'scuba', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'boats', - 'boating', - 'rowboat', - 'canoe', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'snorkel' - ], - modifiable: true), - Emoji( - name: 'person rowing boat: medium skin tone', - char: '\u{1F6A3}\u{1F3FD}', - shortName: 'person_rowing_boat_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'boat', - 'medium skin tone', - 'rowboat', - 'uc8', - 'sport', - 'diversity', - 'boat', - 'rowing', - 'hawaii', - 'scuba', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'boats', - 'boating', - 'rowboat', - 'canoe', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'snorkel' - ], - modifiable: true), - Emoji( - name: 'person rowing boat: medium-dark skin tone', - char: '\u{1F6A3}\u{1F3FE}', - shortName: 'person_rowing_boat_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'boat', - 'medium-dark skin tone', - 'rowboat', - 'uc8', - 'sport', - 'diversity', - 'boat', - 'rowing', - 'hawaii', - 'scuba', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'boats', - 'boating', - 'rowboat', - 'canoe', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'snorkel' - ], - modifiable: true), - Emoji( - name: 'person rowing boat: dark skin tone', - char: '\u{1F6A3}\u{1F3FF}', - shortName: 'person_rowing_boat_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'boat', - 'dark skin tone', - 'rowboat', - 'uc8', - 'sport', - 'diversity', - 'boat', - 'rowing', - 'hawaii', - 'scuba', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'boats', - 'boating', - 'rowboat', - 'canoe', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'snorkel' - ], - modifiable: true), - Emoji( - name: 'woman rowing boat', - char: '\u{1F6A3}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_rowing_boat', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'boat', - 'rowboat', - 'woman', - 'uc6', - 'sport', - 'diversity', - 'boat', - 'rowing', - 'hawaii', - 'scuba', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'boats', - 'boating', - 'rowboat', - 'canoe', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'snorkel' - ]), - Emoji( - name: 'woman rowing boat: light skin tone', - char: '\u{1F6A3}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_rowing_boat_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'boat', - 'light skin tone', - 'rowboat', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'boat', - 'rowing', - 'hawaii', - 'scuba', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'boats', - 'boating', - 'rowboat', - 'canoe', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'snorkel' - ], - modifiable: true), - Emoji( - name: 'woman rowing boat: medium-light skin tone', - char: '\u{1F6A3}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_rowing_boat_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'boat', - 'medium-light skin tone', - 'rowboat', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'boat', - 'rowing', - 'hawaii', - 'scuba', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'boats', - 'boating', - 'rowboat', - 'canoe', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'snorkel' - ], - modifiable: true), - Emoji( - name: 'woman rowing boat: medium skin tone', - char: '\u{1F6A3}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_rowing_boat_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'boat', - 'medium skin tone', - 'rowboat', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'boat', - 'rowing', - 'hawaii', - 'scuba', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'boats', - 'boating', - 'rowboat', - 'canoe', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'snorkel' - ], - modifiable: true), - Emoji( - name: 'woman rowing boat: medium-dark skin tone', - char: '\u{1F6A3}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_rowing_boat_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'boat', - 'medium-dark skin tone', - 'rowboat', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'boat', - 'rowing', - 'hawaii', - 'scuba', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'boats', - 'boating', - 'rowboat', - 'canoe', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'snorkel' - ], - modifiable: true), - Emoji( - name: 'woman rowing boat: dark skin tone', - char: '\u{1F6A3}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_rowing_boat_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'boat', - 'dark skin tone', - 'rowboat', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'boat', - 'rowing', - 'hawaii', - 'scuba', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'boats', - 'boating', - 'rowboat', - 'canoe', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'snorkel' - ], - modifiable: true), - Emoji( - name: 'man rowing boat', - char: '\u{1F6A3}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_rowing_boat', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'boat', - 'man', - 'rowboat', - 'uc6', - 'sport', - 'diversity', - 'boat', - 'rowing', - 'hawaii', - 'scuba', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'boats', - 'boating', - 'rowboat', - 'canoe', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'snorkel' - ]), - Emoji( - name: 'man rowing boat: light skin tone', - char: '\u{1F6A3}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_rowing_boat_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'boat', - 'light skin tone', - 'man', - 'rowboat', - 'uc8', - 'sport', - 'diversity', - 'boat', - 'rowing', - 'hawaii', - 'scuba', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'boats', - 'boating', - 'rowboat', - 'canoe', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'snorkel' - ], - modifiable: true), - Emoji( - name: 'man rowing boat: medium-light skin tone', - char: '\u{1F6A3}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_rowing_boat_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'boat', - 'man', - 'medium-light skin tone', - 'rowboat', - 'uc8', - 'sport', - 'diversity', - 'boat', - 'rowing', - 'hawaii', - 'scuba', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'boats', - 'boating', - 'rowboat', - 'canoe', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'snorkel' - ], - modifiable: true), - Emoji( - name: 'man rowing boat: medium skin tone', - char: '\u{1F6A3}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_rowing_boat_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'boat', - 'man', - 'medium skin tone', - 'rowboat', - 'uc8', - 'sport', - 'diversity', - 'boat', - 'rowing', - 'hawaii', - 'scuba', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'boats', - 'boating', - 'rowboat', - 'canoe', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'snorkel' - ], - modifiable: true), - Emoji( - name: 'man rowing boat: medium-dark skin tone', - char: '\u{1F6A3}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_rowing_boat_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'boat', - 'man', - 'medium-dark skin tone', - 'rowboat', - 'uc8', - 'sport', - 'diversity', - 'boat', - 'rowing', - 'hawaii', - 'scuba', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'boats', - 'boating', - 'rowboat', - 'canoe', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'snorkel' - ], - modifiable: true), - Emoji( - name: 'man rowing boat: dark skin tone', - char: '\u{1F6A3}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_rowing_boat_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'boat', - 'dark skin tone', - 'man', - 'rowboat', - 'uc8', - 'sport', - 'diversity', - 'boat', - 'rowing', - 'hawaii', - 'scuba', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'boats', - 'boating', - 'rowboat', - 'canoe', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'snorkel' - ], - modifiable: true), - Emoji( - name: 'person climbing', - char: '\u{1F9D7}', - shortName: 'person_climbing', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'climber', - 'uc10', - 'sport', - 'diversity', - 'fun', - 'activity', - 'rock climbing', - 'climb', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'climber' - ]), - Emoji( - name: 'person climbing: light skin tone', - char: '\u{1F9D7}\u{1F3FB}', - shortName: 'person_climbing_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'climber', - 'light skin tone', - 'uc10', - 'sport', - 'diversity', - 'fun', - 'activity', - 'rock climbing', - 'climb', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'climber' - ], - modifiable: true), - Emoji( - name: 'person climbing: medium-light skin tone', - char: '\u{1F9D7}\u{1F3FC}', - shortName: 'person_climbing_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'climber', - 'medium-light skin tone', - 'uc10', - 'sport', - 'diversity', - 'fun', - 'activity', - 'rock climbing', - 'climb', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'climber' - ], - modifiable: true), - Emoji( - name: 'person climbing: medium skin tone', - char: '\u{1F9D7}\u{1F3FD}', - shortName: 'person_climbing_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'climber', - 'medium skin tone', - 'uc10', - 'sport', - 'diversity', - 'fun', - 'activity', - 'rock climbing', - 'climb', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'climber' - ], - modifiable: true), - Emoji( - name: 'person climbing: medium-dark skin tone', - char: '\u{1F9D7}\u{1F3FE}', - shortName: 'person_climbing_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'climber', - 'medium-dark skin tone', - 'uc10', - 'sport', - 'diversity', - 'fun', - 'activity', - 'rock climbing', - 'climb', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'climber' - ], - modifiable: true), - Emoji( - name: 'person climbing: dark skin tone', - char: '\u{1F9D7}\u{1F3FF}', - shortName: 'person_climbing_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'climber', - 'dark skin tone', - 'uc10', - 'sport', - 'diversity', - 'fun', - 'activity', - 'rock climbing', - 'climb', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'climber' - ], - modifiable: true), - Emoji( - name: 'woman climbing', - char: '\u{1F9D7}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_climbing', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'climber', - 'uc10', - 'sport', - 'diversity', - 'fun', - 'activity', - 'rock climbing', - 'climb', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'climber' - ]), - Emoji( - name: 'woman climbing: light skin tone', - char: '\u{1F9D7}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_climbing_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'climber', - 'light skin tone', - 'uc10', - 'sport', - 'diversity', - 'fun', - 'activity', - 'rock climbing', - 'climb', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'climber' - ], - modifiable: true), - Emoji( - name: 'woman climbing: medium-light skin tone', - char: '\u{1F9D7}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_climbing_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'climber', - 'medium-light skin tone', - 'uc10', - 'sport', - 'diversity', - 'fun', - 'activity', - 'rock climbing', - 'climb', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'climber' - ], - modifiable: true), - Emoji( - name: 'woman climbing: medium skin tone', - char: '\u{1F9D7}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_climbing_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'climber', - 'medium skin tone', - 'uc10', - 'sport', - 'diversity', - 'fun', - 'activity', - 'rock climbing', - 'climb', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'climber' - ], - modifiable: true), - Emoji( - name: 'woman climbing: medium-dark skin tone', - char: '\u{1F9D7}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_climbing_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'climber', - 'medium-dark skin tone', - 'uc10', - 'sport', - 'diversity', - 'fun', - 'activity', - 'rock climbing', - 'climb', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'climber' - ], - modifiable: true), - Emoji( - name: 'woman climbing: dark skin tone', - char: '\u{1F9D7}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_climbing_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'climber', - 'dark skin tone', - 'uc10', - 'sport', - 'diversity', - 'fun', - 'activity', - 'rock climbing', - 'climb', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'climber' - ], - modifiable: true), - Emoji( - name: 'man climbing', - char: '\u{1F9D7}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_climbing', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'climber', - 'uc10', - 'sport', - 'diversity', - 'fun', - 'activity', - 'rock climbing', - 'climb', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'climber' - ]), - Emoji( - name: 'man climbing: light skin tone', - char: '\u{1F9D7}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_climbing_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'climber', - 'light skin tone', - 'uc10', - 'sport', - 'diversity', - 'fun', - 'activity', - 'rock climbing', - 'climb', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'climber' - ], - modifiable: true), - Emoji( - name: 'man climbing: medium-light skin tone', - char: '\u{1F9D7}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_climbing_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'climber', - 'medium-light skin tone', - 'uc10', - 'sport', - 'diversity', - 'fun', - 'activity', - 'rock climbing', - 'climb', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'climber' - ], - modifiable: true), - Emoji( - name: 'man climbing: medium skin tone', - char: '\u{1F9D7}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_climbing_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'climber', - 'medium skin tone', - 'uc10', - 'sport', - 'diversity', - 'fun', - 'activity', - 'rock climbing', - 'climb', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'climber' - ], - modifiable: true), - Emoji( - name: 'man climbing: medium-dark skin tone', - char: '\u{1F9D7}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_climbing_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'climber', - 'medium-dark skin tone', - 'uc10', - 'sport', - 'diversity', - 'fun', - 'activity', - 'rock climbing', - 'climb', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'climber' - ], - modifiable: true), - Emoji( - name: 'man climbing: dark skin tone', - char: '\u{1F9D7}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_climbing_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personActivity, - keywords: [ - 'climber', - 'dark skin tone', - 'uc10', - 'sport', - 'diversity', - 'fun', - 'activity', - 'rock climbing', - 'climb', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'climber' - ], - modifiable: true), - Emoji( - name: 'person mountain biking', - char: '\u{1F6B5}', - shortName: 'person_mountain_biking', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'bicyclist', - 'bike', - 'cyclist', - 'mountain', - 'uc6', - 'sport', - 'diversity', - 'bike', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling' - ]), - Emoji( - name: 'person mountain biking: light skin tone', - char: '\u{1F6B5}\u{1F3FB}', - shortName: 'person_mountain_biking_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'bicyclist', - 'bike', - 'cyclist', - 'light skin tone', - 'mountain', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling' - ], - modifiable: true), - Emoji( - name: 'person mountain biking: medium-light skin tone', - char: '\u{1F6B5}\u{1F3FC}', - shortName: 'person_mountain_biking_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'bicyclist', - 'bike', - 'cyclist', - 'medium-light skin tone', - 'mountain', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling' - ], - modifiable: true), - Emoji( - name: 'person mountain biking: medium skin tone', - char: '\u{1F6B5}\u{1F3FD}', - shortName: 'person_mountain_biking_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'bicyclist', - 'bike', - 'cyclist', - 'medium skin tone', - 'mountain', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling' - ], - modifiable: true), - Emoji( - name: 'person mountain biking: medium-dark skin tone', - char: '\u{1F6B5}\u{1F3FE}', - shortName: 'person_mountain_biking_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'bicyclist', - 'bike', - 'cyclist', - 'medium-dark skin tone', - 'mountain', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling' - ], - modifiable: true), - Emoji( - name: 'person mountain biking: dark skin tone', - char: '\u{1F6B5}\u{1F3FF}', - shortName: 'person_mountain_biking_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'bicyclist', - 'bike', - 'cyclist', - 'dark skin tone', - 'mountain', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling' - ], - modifiable: true), - Emoji( - name: 'woman mountain biking', - char: '\u{1F6B5}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_mountain_biking', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'bike', - 'biking', - 'cyclist', - 'mountain', - 'woman', - 'uc6', - 'sport', - 'diversity', - 'bike', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling' - ]), - Emoji( - name: 'woman mountain biking: light skin tone', - char: '\u{1F6B5}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_mountain_biking_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'bike', - 'biking', - 'cyclist', - 'light skin tone', - 'mountain', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling' - ], - modifiable: true), - Emoji( - name: 'woman mountain biking: medium-light skin tone', - char: '\u{1F6B5}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_mountain_biking_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'bike', - 'biking', - 'cyclist', - 'medium-light skin tone', - 'mountain', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling' - ], - modifiable: true), - Emoji( - name: 'woman mountain biking: medium skin tone', - char: '\u{1F6B5}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_mountain_biking_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'bike', - 'biking', - 'cyclist', - 'medium skin tone', - 'mountain', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling' - ], - modifiable: true), - Emoji( - name: 'woman mountain biking: medium-dark skin tone', - char: '\u{1F6B5}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_mountain_biking_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'bike', - 'biking', - 'cyclist', - 'medium-dark skin tone', - 'mountain', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling' - ], - modifiable: true), - Emoji( - name: 'woman mountain biking: dark skin tone', - char: '\u{1F6B5}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_mountain_biking_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'bike', - 'biking', - 'cyclist', - 'dark skin tone', - 'mountain', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling' - ], - modifiable: true), - Emoji( - name: 'man mountain biking', - char: '\u{1F6B5}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_mountain_biking', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'bike', - 'cyclist', - 'man', - 'mountain', - 'uc6', - 'sport', - 'diversity', - 'bike', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling' - ]), - Emoji( - name: 'man mountain biking: light skin tone', - char: '\u{1F6B5}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_mountain_biking_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'bike', - 'cyclist', - 'light skin tone', - 'man', - 'mountain', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling' - ], - modifiable: true), - Emoji( - name: 'man mountain biking: medium-light skin tone', - char: '\u{1F6B5}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_mountain_biking_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'bike', - 'cyclist', - 'man', - 'medium-light skin tone', - 'mountain', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling' - ], - modifiable: true), - Emoji( - name: 'man mountain biking: medium skin tone', - char: '\u{1F6B5}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_mountain_biking_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'bike', - 'cyclist', - 'man', - 'medium skin tone', - 'mountain', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling' - ], - modifiable: true), - Emoji( - name: 'man mountain biking: medium-dark skin tone', - char: '\u{1F6B5}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_mountain_biking_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'bike', - 'cyclist', - 'man', - 'medium-dark skin tone', - 'mountain', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling' - ], - modifiable: true), - Emoji( - name: 'man mountain biking: dark skin tone', - char: '\u{1F6B5}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_mountain_biking_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'bike', - 'cyclist', - 'dark skin tone', - 'man', - 'mountain', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling' - ], - modifiable: true), - Emoji( - name: 'person biking', - char: '\u{1F6B4}', - shortName: 'person_biking', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'biking', - 'cyclist', - 'uc6', - 'sport', - 'diversity', - 'bike', - 'fame', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling', - 'famous', - 'celebrity' - ]), - Emoji( - name: 'person biking: light skin tone', - char: '\u{1F6B4}\u{1F3FB}', - shortName: 'person_biking_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'biking', - 'cyclist', - 'light skin tone', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fame', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'person biking: medium-light skin tone', - char: '\u{1F6B4}\u{1F3FC}', - shortName: 'person_biking_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'biking', - 'cyclist', - 'medium-light skin tone', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fame', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'person biking: medium skin tone', - char: '\u{1F6B4}\u{1F3FD}', - shortName: 'person_biking_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'biking', - 'cyclist', - 'medium skin tone', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fame', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'person biking: medium-dark skin tone', - char: '\u{1F6B4}\u{1F3FE}', - shortName: 'person_biking_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'biking', - 'cyclist', - 'medium-dark skin tone', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fame', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'person biking: dark skin tone', - char: '\u{1F6B4}\u{1F3FF}', - shortName: 'person_biking_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'biking', - 'cyclist', - 'dark skin tone', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fame', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'woman biking', - char: '\u{1F6B4}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_biking', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'biking', - 'cyclist', - 'woman', - 'uc6', - 'sport', - 'diversity', - 'bike', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling' - ]), - Emoji( - name: 'woman biking: light skin tone', - char: '\u{1F6B4}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_biking_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'biking', - 'cyclist', - 'light skin tone', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling' - ], - modifiable: true), - Emoji( - name: 'woman biking: medium-light skin tone', - char: '\u{1F6B4}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_biking_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'biking', - 'cyclist', - 'medium-light skin tone', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling' - ], - modifiable: true), - Emoji( - name: 'woman biking: medium skin tone', - char: '\u{1F6B4}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_biking_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'biking', - 'cyclist', - 'medium skin tone', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling' - ], - modifiable: true), - Emoji( - name: 'woman biking: medium-dark skin tone', - char: '\u{1F6B4}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_biking_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'biking', - 'cyclist', - 'medium-dark skin tone', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling' - ], - modifiable: true), - Emoji( - name: 'woman biking: dark skin tone', - char: '\u{1F6B4}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_biking_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'biking', - 'cyclist', - 'dark skin tone', - 'woman', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling' - ], - modifiable: true), - Emoji( - name: 'man biking', - char: '\u{1F6B4}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_biking', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'biking', - 'cyclist', - 'man', - 'uc6', - 'sport', - 'diversity', - 'bike', - 'fame', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling', - 'famous', - 'celebrity' - ]), - Emoji( - name: 'man biking: light skin tone', - char: '\u{1F6B4}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_biking_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'biking', - 'cyclist', - 'light skin tone', - 'man', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fame', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'man biking: medium-light skin tone', - char: '\u{1F6B4}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_biking_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'biking', - 'cyclist', - 'man', - 'medium-light skin tone', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fame', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'man biking: medium skin tone', - char: '\u{1F6B4}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_biking_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'biking', - 'cyclist', - 'man', - 'medium skin tone', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fame', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'man biking: medium-dark skin tone', - char: '\u{1F6B4}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_biking_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'biking', - 'cyclist', - 'man', - 'medium-dark skin tone', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fame', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'man biking: dark skin tone', - char: '\u{1F6B4}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_biking_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'bicycle', - 'biking', - 'cyclist', - 'dark skin tone', - 'man', - 'uc8', - 'sport', - 'diversity', - 'bike', - 'fame', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'bikes', - 'bicycle', - 'bicycling', - 'famous', - 'celebrity' - ], - modifiable: true), - Emoji( - name: 'trophy', - char: '\u{1F3C6}', - shortName: 'trophy', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.awardMedal, - keywords: [ - 'prize', - 'uc6', - 'sport', - 'game', - 'award', - 'football', - 'soccer', - 'perfect', - 'win', - 'harry potter', - 'nerd', - 'fame', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'games', - 'gaming', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'soccer ball', - 'world cup', - 'perfecto', - 'perfection', - 'superb', - 'flawless', - 'excellent', - 'supreme', - 'super', - 'great', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'smart', - 'geek', - 'serious', - 'famous', - 'celebrity' - ]), - Emoji( - name: '1st place medal', - char: '\u{1F947}', - shortName: 'first_place', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.awardMedal, - keywords: [ - 'first', - 'gold', - 'medal', - 'uc9', - 'sport', - 'award', - 'win', - 'medal', - 'gymnast', - 'fame', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'medals', - 'gold medal', - 'silver medal', - 'bronze medal', - 'gymnastics', - 'famous', - 'celebrity' - ]), - Emoji( - name: '2nd place medal', - char: '\u{1F948}', - shortName: 'second_place', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.awardMedal, - keywords: [ - 'medal', - 'second', - 'silver', - 'uc9', - 'sport', - 'award', - 'win', - 'medal', - 'gymnast', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'medals', - 'gold medal', - 'silver medal', - 'bronze medal', - 'gymnastics' - ]), - Emoji( - name: '3rd place medal', - char: '\u{1F949}', - shortName: 'third_place', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.awardMedal, - keywords: [ - 'bronze', - 'medal', - 'third', - 'uc9', - 'sport', - 'award', - 'win', - 'medal', - 'gymnast', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'medals', - 'gold medal', - 'silver medal', - 'bronze medal', - 'gymnastics' - ]), - Emoji( - name: 'sports medal', - char: '\u{1F3C5}', - shortName: 'medal', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.awardMedal, - keywords: [ - 'medal', - 'uc7', - 'sport', - 'award', - 'perfect', - 'win', - 'medal', - 'gymnast', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'perfecto', - 'perfection', - 'superb', - 'flawless', - 'excellent', - 'supreme', - 'super', - 'great', - 'winning', - 'killing it', - 'crushing it', - 'victory', - 'victorious', - 'success', - 'successful', - 'winner', - 'medals', - 'gold medal', - 'silver medal', - 'bronze medal', - 'gymnastics' - ]), - Emoji( - name: 'military medal', - char: '\u{1F396}\u{FE0F}', - shortName: 'military_medal', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.awardMedal, - keywords: [ - 'celebration', - 'medal', - 'military', - 'uc7', - 'award', - 'medal', - 'gymnast', - 'activity', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'medals', - 'gold medal', - 'silver medal', - 'bronze medal', - 'gymnastics' - ]), - Emoji( - name: 'rosette', - char: '\u{1F3F5}\u{FE0F}', - shortName: 'rosette', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.plantFlower, - keywords: ['plant', 'uc7', 'tropical', 'activity']), - Emoji( - name: 'reminder ribbon', - char: '\u{1F397}\u{FE0F}', - shortName: 'reminder_ribbon', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.event, - keywords: [ - 'celebration', - 'reminder', - 'ribbon', - 'uc7', - 'award', - 'hope', - 'activity', - 'important', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'swear', - 'promise' - ]), - Emoji( - name: 'ticket', - char: '\u{1F3AB}', - shortName: 'ticket', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.event, - keywords: [ - 'admission', - 'uc6', - 'theatre', - 'instruments', - 'movie', - 'amusement park', - 'circus', - 'pink', - 'disney', - 'discount', - 'las vegas', - 'activity', - 'opera', - 'theater', - 'craft', - 'drama', - 'monet', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'movies', - 'cinema', - 'film', - 'films', - 'video', - 'videos', - 'theme park', - 'circus tent', - 'clown', - 'clowns', - 'rose', - 'cartoon', - 'sale', - 'bargain', - 'vegas' - ]), - Emoji( - name: 'admission tickets', - char: '\u{1F39F}\u{FE0F}', - shortName: 'tickets', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.event, - keywords: [ - 'admission', - 'ticket', - 'uc7', - 'theatre', - 'instruments', - 'movie', - 'amusement park', - 'circus', - 'disney', - 'activity', - 'theater', - 'craft', - 'drama', - 'monet', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'movies', - 'cinema', - 'film', - 'films', - 'video', - 'videos', - 'theme park', - 'circus tent', - 'clown', - 'clowns', - 'cartoon' - ]), - Emoji( - name: 'circus tent', - char: '\u{1F3AA}', - shortName: 'circus_tent', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeOther, - keywords: [ - 'circus', - 'tent', - 'uc6', - 'amusement park', - 'circus', - 'magic', - 'activity', - 'independence day', - 'opera', - 'theme park', - 'circus tent', - 'clown', - 'clowns', - 'spell', - 'genie', - 'magical', - '4th of july' - ]), - Emoji( - name: 'person juggling', - char: '\u{1F939}', - shortName: 'person_juggling', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'balance', - 'juggle', - 'multitask', - 'skill', - 'uc9', - 'diversity', - 'ball', - 'circus', - 'throw', - 'activity', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'circus tent', - 'clown', - 'clowns' - ]), - Emoji( - name: 'person juggling: light skin tone', - char: '\u{1F939}\u{1F3FB}', - shortName: 'person_juggling_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'balance', - 'juggle', - 'light skin tone', - 'multitask', - 'skill', - 'uc9', - 'diversity', - 'ball', - 'circus', - 'throw', - 'activity', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'circus tent', - 'clown', - 'clowns' - ], - modifiable: true), - Emoji( - name: 'person juggling: medium-light skin tone', - char: '\u{1F939}\u{1F3FC}', - shortName: 'person_juggling_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'balance', - 'juggle', - 'medium-light skin tone', - 'multitask', - 'skill', - 'uc9', - 'diversity', - 'ball', - 'circus', - 'throw', - 'activity', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'circus tent', - 'clown', - 'clowns' - ], - modifiable: true), - Emoji( - name: 'person juggling: medium skin tone', - char: '\u{1F939}\u{1F3FD}', - shortName: 'person_juggling_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'balance', - 'juggle', - 'medium skin tone', - 'multitask', - 'skill', - 'uc9', - 'diversity', - 'ball', - 'circus', - 'throw', - 'activity', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'circus tent', - 'clown', - 'clowns' - ], - modifiable: true), - Emoji( - name: 'person juggling: medium-dark skin tone', - char: '\u{1F939}\u{1F3FE}', - shortName: 'person_juggling_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'balance', - 'juggle', - 'medium-dark skin tone', - 'multitask', - 'skill', - 'uc9', - 'diversity', - 'ball', - 'circus', - 'throw', - 'activity', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'circus tent', - 'clown', - 'clowns' - ], - modifiable: true), - Emoji( - name: 'person juggling: dark skin tone', - char: '\u{1F939}\u{1F3FF}', - shortName: 'person_juggling_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'balance', - 'dark skin tone', - 'juggle', - 'multitask', - 'skill', - 'uc9', - 'diversity', - 'ball', - 'circus', - 'throw', - 'activity', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'circus tent', - 'clown', - 'clowns' - ], - modifiable: true), - Emoji( - name: 'woman juggling', - char: '\u{1F939}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_juggling', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'juggling', - 'multitask', - 'woman', - 'uc9', - 'diversity', - 'ball', - 'circus', - 'throw', - 'activity', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'circus tent', - 'clown', - 'clowns' - ]), - Emoji( - name: 'woman juggling: light skin tone', - char: '\u{1F939}\u{1F3FB}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_juggling_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'juggling', - 'light skin tone', - 'multitask', - 'woman', - 'uc9', - 'diversity', - 'ball', - 'circus', - 'throw', - 'activity', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'circus tent', - 'clown', - 'clowns' - ], - modifiable: true), - Emoji( - name: 'woman juggling: medium-light skin tone', - char: '\u{1F939}\u{1F3FC}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_juggling_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'juggling', - 'medium-light skin tone', - 'multitask', - 'woman', - 'uc9', - 'diversity', - 'ball', - 'circus', - 'throw', - 'activity', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'circus tent', - 'clown', - 'clowns' - ], - modifiable: true), - Emoji( - name: 'woman juggling: medium skin tone', - char: '\u{1F939}\u{1F3FD}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_juggling_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'juggling', - 'medium skin tone', - 'multitask', - 'woman', - 'uc9', - 'diversity', - 'ball', - 'circus', - 'throw', - 'activity', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'circus tent', - 'clown', - 'clowns' - ], - modifiable: true), - Emoji( - name: 'woman juggling: medium-dark skin tone', - char: '\u{1F939}\u{1F3FE}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_juggling_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'juggling', - 'medium-dark skin tone', - 'multitask', - 'woman', - 'uc9', - 'diversity', - 'ball', - 'circus', - 'throw', - 'activity', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'circus tent', - 'clown', - 'clowns' - ], - modifiable: true), - Emoji( - name: 'woman juggling: dark skin tone', - char: '\u{1F939}\u{1F3FF}\u{200D}\u{2640}\u{FE0F}', - shortName: 'woman_juggling_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'dark skin tone', - 'juggling', - 'multitask', - 'woman', - 'uc9', - 'diversity', - 'ball', - 'circus', - 'throw', - 'activity', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'circus tent', - 'clown', - 'clowns' - ], - modifiable: true), - Emoji( - name: 'man juggling', - char: '\u{1F939}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_juggling', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'juggling', - 'man', - 'multitask', - 'uc9', - 'diversity', - 'ball', - 'circus', - 'throw', - 'activity', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'circus tent', - 'clown', - 'clowns' - ]), - Emoji( - name: 'man juggling: light skin tone', - char: '\u{1F939}\u{1F3FB}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_juggling_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'juggling', - 'light skin tone', - 'man', - 'multitask', - 'uc9', - 'diversity', - 'ball', - 'circus', - 'throw', - 'activity', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'circus tent', - 'clown', - 'clowns' - ], - modifiable: true), - Emoji( - name: 'man juggling: medium-light skin tone', - char: '\u{1F939}\u{1F3FC}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_juggling_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'juggling', - 'man', - 'medium-light skin tone', - 'multitask', - 'uc9', - 'diversity', - 'ball', - 'circus', - 'throw', - 'activity', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'circus tent', - 'clown', - 'clowns' - ], - modifiable: true), - Emoji( - name: 'man juggling: medium skin tone', - char: '\u{1F939}\u{1F3FD}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_juggling_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'juggling', - 'man', - 'medium skin tone', - 'multitask', - 'uc9', - 'diversity', - 'ball', - 'circus', - 'throw', - 'activity', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'circus tent', - 'clown', - 'clowns' - ], - modifiable: true), - Emoji( - name: 'man juggling: medium-dark skin tone', - char: '\u{1F939}\u{1F3FE}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_juggling_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'juggling', - 'man', - 'medium-dark skin tone', - 'multitask', - 'uc9', - 'diversity', - 'ball', - 'circus', - 'throw', - 'activity', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'circus tent', - 'clown', - 'clowns' - ], - modifiable: true), - Emoji( - name: 'man juggling: dark skin tone', - char: '\u{1F939}\u{1F3FF}\u{200D}\u{2642}\u{FE0F}', - shortName: 'man_juggling_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personSport, - keywords: [ - 'dark skin tone', - 'juggling', - 'man', - 'multitask', - 'uc9', - 'diversity', - 'ball', - 'circus', - 'throw', - 'activity', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'balls', - 'ballon', - 'circus tent', - 'clown', - 'clowns' - ], - modifiable: true), - Emoji( - name: 'performing arts', - char: '\u{1F3AD}', - shortName: 'performing_arts', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.artsCrafts, - keywords: [ - 'art', - 'mask', - 'performing', - 'theater', - 'theatre', - 'uc6', - 'theatre', - 'halloween', - 'movie', - 'circus', - 'girls night', - 'play', - 'fame', - 'las vegas', - 'fun', - 'activity', - 'mask', - 'fantasy', - 'opera', - 'theater', - 'craft', - 'drama', - 'monet', - 'samhain', - 'movies', - 'cinema', - 'film', - 'films', - 'video', - 'videos', - 'circus tent', - 'clown', - 'clowns', - 'ladies night', - 'girls only', - 'girlfriend', - 'famous', - 'celebrity', - 'vegas' - ]), - Emoji( - name: 'ballet shoes', - char: '\u{1FA70}', - shortName: 'ballet_shoes', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'uc12', - 'shoe', - 'dance', - 'vintage', - 'activity', - 'opera', - 'shoes', - 'baskets', - 'loafers', - 'sandals', - 'pumps', - 'boots', - 'heels', - 'dancers', - 'dancing', - 'ballet', - 'ballerina', - 'dabbing', - 'salsa' - ]), - Emoji( - name: 'artist palette', - char: '\u{1F3A8}', - shortName: 'art', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.artsCrafts, - keywords: [ - 'art', - 'museum', - 'painting', - 'palette', - 'uc6', - 'theatre', - 'painting', - 'color', - 'instagram', - 'fun', - 'activity', - 'theater', - 'craft', - 'drama', - 'monet', - 'painter', - 'arts', - 'colour', - 'coloring', - 'colouring', - 'drawing', - 'marker', - 'sketch' - ]), - Emoji( - name: 'clapper board', - char: '\u{1F3AC}', - shortName: 'clapper', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.lightVideo, - keywords: [ - 'clapper', - 'movie', - 'uc6', - 'movie', - 'disney', - 'california', - 'fame', - 'activity', - 'movies', - 'cinema', - 'film', - 'films', - 'video', - 'videos', - 'cartoon', - 'famous', - 'celebrity' - ]), - Emoji( - name: 'microphone', - char: '\u{1F3A4}', - shortName: 'microphone', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.music, - keywords: [ - 'karaoke', - 'mic', - 'uc6', - 'instruments', - 'rock and roll', - 'disco', - 'fame', - 'activity', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'famous', - 'celebrity' - ]), - Emoji( - name: 'headphone', - char: '\u{1F3A7}', - shortName: 'headphones', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.music, - keywords: [ - 'earbud', - 'uc6', - 'instruments', - 'headphones', - 'rock and roll', - 'earphone', - 'activity', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'headphone', - 'head phones', - 'casque', - 'earbud' - ]), - Emoji( - name: 'musical score', - char: '\u{1F3BC}', - shortName: 'musical_score', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.music, - keywords: [ - 'music', - 'score', - 'uc6', - 'instruments', - 'piano', - 'rock and roll', - 'disco', - 'activity', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique' - ]), - Emoji( - name: 'musical keyboard', - char: '\u{1F3B9}', - shortName: 'musical_keyboard', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.musicalInstrument, - keywords: [ - 'instrument', - 'keyboard', - 'music', - 'piano', - 'uc6', - 'instruments', - 'play', - 'keyboard', - 'piano', - 'rock and roll', - 'activity', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'keyboards' - ]), - Emoji( - name: 'drum', - char: '\u{1F941}', - shortName: 'drum', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.musicalInstrument, - keywords: [ - 'drum', - 'drumsticks', - 'music', - 'uc9', - 'instruments', - 'play', - 'rock and roll', - 'activity', - 'toy', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique' - ]), - Emoji( - name: 'long drum', - char: '\u{1FA98}', - shortName: 'long_drum', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.musicalInstrument, - keywords: [ - 'uc13', - 'instruments', - 'play', - 'activity', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique' - ]), - Emoji( - name: 'saxophone', - char: '\u{1F3B7}', - shortName: 'saxophone', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.musicalInstrument, - keywords: [ - 'instrument', - 'music', - 'sax', - 'uc6', - 'instruments', - 'play', - 'activity', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique' - ]), - Emoji( - name: 'trumpet', - char: '\u{1F3BA}', - shortName: 'trumpet', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.musicalInstrument, - keywords: [ - 'instrument', - 'music', - 'uc6', - 'instruments', - 'play', - 'activity', - 'independence day', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - '4th of july' - ]), - Emoji( - name: 'guitar', - char: '\u{1F3B8}', - shortName: 'guitar', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.musicalInstrument, - keywords: [ - 'instrument', - 'music', - 'uc6', - 'instruments', - 'mexican', - 'play', - 'rock and roll', - 'activity', - 'guitarra', - 'stringed', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'mexico', - 'cinco de mayo', - 'español', - 'guitare', - 'gitarre', - 'chitarra', - 'bangio' - ]), - Emoji( - name: 'banjo', - char: '\u{1FA95}', - shortName: 'banjo', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.musicalInstrument, - keywords: [ - 'uc12', - 'instruments', - 'play', - 'activity', - 'toy', - 'guitarra', - 'stringed', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'guitare', - 'gitarre', - 'chitarra', - 'bangio' - ]), - Emoji( - name: 'violin', - char: '\u{1F3BB}', - shortName: 'violin', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.musicalInstrument, - keywords: [ - 'instrument', - 'music', - 'uc6', - 'instruments', - 'sarcastic', - 'play', - 'activity', - 'stringed', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'sarcasm' - ]), - Emoji( - name: 'accordion', - char: '\u{1FA97}', - shortName: 'accordion', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.musicalInstrument, - keywords: [ - 'uc13', - 'instruments', - 'play', - 'keyboard', - 'activity', - 'squeezebox', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'keyboards', - 'akkordeon', - 'bellows', - 'aerophone' - ]), - Emoji( - name: 'game die', - char: '\u{1F3B2}', - shortName: 'game_die', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.game, - keywords: [ - 'dice', - 'die', - 'game', - 'uc6', - 'game', - 'boys night', - 'play', - 'bingo', - 'las vegas', - 'dice', - 'fun', - 'activity', - 'toy', - 'games', - 'gaming', - 'guys night', - 'vegas' - ]), - Emoji( - name: 'chess pawn', - char: '\u{265F}\u{FE0F}', - shortName: 'chess_pawn', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.game, - keywords: ['uc1', 'game', 'play', 'fun', 'activity', 'games', 'gaming']), - Emoji( - name: 'direct hit', - char: '\u{1F3AF}', - shortName: 'dart', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.game, - keywords: [ - 'bull', - 'bullseye', - 'dart', - 'eye', - 'game', - 'hit', - 'target', - 'uc6', - 'sport', - 'game', - 'boys night', - 'play', - 'target', - 'fun', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'games', - 'gaming', - 'guys night' - ]), - Emoji( - name: 'bowling', - char: '\u{1F3B3}', - shortName: 'bowling', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.sport, - keywords: [ - 'ball', - 'game', - 'uc6', - 'sport', - 'game', - 'ball', - 'boys night', - 'play', - 'fun', - 'throw', - 'activity', - 'sports', - 'exercise', - 'athlete', - 'athletes', - 'athletic', - 'team', - 'fitness', - 'work out', - 'workout', - 'games', - 'gaming', - 'balls', - 'ballon', - 'guys night' - ]), - Emoji( - name: 'video game', - char: '\u{1F3AE}', - shortName: 'video_game', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.game, - keywords: [ - 'controller', - 'game', - 'uc6', - 'electronics', - 'game', - 'boys night', - 'play', - 'controller', - 'fun', - 'activity', - 'games', - 'gaming', - 'guys night', - 'remote' - ]), - Emoji( - name: 'slot machine', - char: '\u{1F3B0}', - shortName: 'slot_machine', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.game, - keywords: [ - 'game', - 'slot', - 'uc6', - 'game', - 'boys night', - 'play', - 'bingo', - 'las vegas', - 'fun', - 'activity', - 'games', - 'gaming', - 'guys night', - 'vegas' - ]), - Emoji( - name: 'puzzle piece', - char: '\u{1F9E9}', - shortName: 'jigsaw', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.game, - keywords: [ - 'uc11', - 'game', - 'play', - 'fun', - 'puzzle', - 'activity', - 'household', - 'toy', - 'question', - 'games', - 'gaming', - 'quiz', - 'puzzled' - ]), - Emoji( - name: 'automobile', - char: '\u{1F697}', - shortName: 'red_car', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'car', - 'uc6', - 'transportation', - 'car', - 'travel', - 'toy', - 'cars', - 'vehicle', - 'fast car', - 'drive', - 'driving', - 'auto' - ]), - Emoji( - name: 'taxi', - char: '\u{1F695}', - shortName: 'taxi', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'vehicle', - 'uc6', - 'transportation', - 'car', - 'travel', - 'cars', - 'vehicle', - 'fast car', - 'drive', - 'driving', - 'auto' - ]), - Emoji( - name: 'sport utility vehicle', - char: '\u{1F699}', - shortName: 'blue_car', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'recreational', - 'sport utility', - 'uc6', - 'transportation', - 'car', - 'travel', - 'cars', - 'vehicle', - 'fast car', - 'drive', - 'driving', - 'auto' - ]), - Emoji( - name: 'pickup truck', - char: '\u{1F6FB}', - shortName: 'pickup_truck', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'uc13', - 'transportation', - 'car', - 'truck', - 'travel', - 'cars', - 'vehicle', - 'fast car', - 'drive', - 'driving', - 'auto', - 'trucks' - ]), - Emoji( - name: 'bus', - char: '\u{1F68C}', - shortName: 'bus', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'vehicle', - 'uc6', - 'transportation', - 'bus', - 'classroom', - 'travel', - 'vacation', - 'buses', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning' - ]), - Emoji( - name: 'trolleybus', - char: '\u{1F68E}', - shortName: 'trolleybus', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'bus', - 'tram', - 'trolley', - 'uc6', - 'transportation', - 'bus', - 'travel', - 'buses' - ]), - Emoji( - name: 'racing car', - char: '\u{1F3CE}\u{FE0F}', - shortName: 'race_car', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'car', - 'racing', - 'uc7', - 'transportation', - 'car', - 'disney', - 'fun', - 'rich', - 'cars', - 'vehicle', - 'fast car', - 'drive', - 'driving', - 'auto', - 'cartoon', - 'grand', - 'expensive', - 'fancy' - ]), - Emoji( - name: 'police car', - char: '\u{1F693}', - shortName: 'police_car', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'car', - 'patrol', - 'police', - 'uc6', - 'transportation', - 'car', - 'police', - '911', - 'sirens', - 'help', - 'cars', - 'vehicle', - 'fast car', - 'drive', - 'driving', - 'auto', - 'cop', - 'policeman', - 'popo', - 'prison', - 'handcuff', - 'jail', - 'justice', - 'emergency', - 'injury', - 'switch' - ]), - Emoji( - name: 'ambulance', - char: '\u{1F691}', - shortName: 'ambulance', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'vehicle', - 'uc6', - 'transportation', - '911', - 'sirens', - 'help', - 'poison', - 'covid', - 'emergency', - 'injury', - 'switch', - 'toxic', - 'toxins' - ]), - Emoji( - name: 'fire engine', - char: '\u{1F692}', - shortName: 'fire_engine', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'engine', - 'fire', - 'truck', - 'uc6', - 'transportation', - 'truck', - '911', - 'sirens', - 'help', - 'fires', - 'trucks', - 'emergency', - 'injury', - 'switch' - ]), - Emoji( - name: 'minibus', - char: '\u{1F690}', - shortName: 'minibus', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'bus', - 'uc6', - 'transportation', - 'bus', - 'travel', - 'camp', - 'vacation', - 'buses', - 'camping', - 'tent', - 'camper', - 'outdoor', - 'outside' - ]), - Emoji( - name: 'delivery truck', - char: '\u{1F69A}', - shortName: 'truck', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'delivery', - 'truck', - 'uc6', - 'transportation', - 'truck', - 'moving', - 'trucks' - ]), - Emoji( - name: 'articulated lorry', - char: '\u{1F69B}', - shortName: 'articulated_lorry', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'lorry', - 'semi', - 'truck', - 'uc6', - 'transportation', - 'truck', - 'moving', - 'trucks' - ]), - Emoji( - name: 'tractor', - char: '\u{1F69C}', - shortName: 'tractor', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: ['vehicle', 'uc6', 'transportation', 'farm']), - Emoji( - name: 'white cane', - char: '\u{1F9AF}', - shortName: 'probing_cane', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.tool, - keywords: [ - 'uc12', - 'transportation', - 'cane', - 'handicap', - 'navigate', - 'blind', - 'probe', - 'accessibility', - 'disabled', - 'disability', - 'white cane' - ]), - Emoji( - name: 'manual wheelchair', - char: '\u{1F9BD}', - shortName: 'manual_wheelchair', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'uc12', - 'transportation', - 'handicap', - 'accessibility', - 'disabled', - 'disability' - ]), - Emoji( - name: 'motorized wheelchair', - char: '\u{1F9BC}', - shortName: 'motorized_wheelchair', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'uc12', - 'transportation', - 'handicap', - 'accessibility', - 'disabled', - 'disability' - ]), - Emoji( - name: 'kick scooter', - char: '\u{1F6F4}', - shortName: 'scooter', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: ['kick', 'scooter', 'uc9', 'transportation']), - Emoji( - name: 'bicycle', - char: '\u{1F6B2}', - shortName: 'bike', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'bike', - 'uc6', - 'transportation', - 'bike', - 'travel', - 'bikes', - 'bicycle', - 'bicycling' - ]), - Emoji( - name: 'motor scooter', - char: '\u{1F6F5}', - shortName: 'motor_scooter', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'motor', - 'scooter', - 'uc9', - 'transportation', - 'travel', - 'thai', - 'pattaya' - ]), - Emoji( - name: 'motorcycle', - char: '\u{1F3CD}\u{FE0F}', - shortName: 'motorcycle', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'racing', - 'uc7', - 'transportation', - 'bike', - 'travel', - 'super hero', - 'fun', - 'bikes', - 'bicycle', - 'bicycling', - 'superhero', - 'superman', - 'batman' - ]), - Emoji( - name: 'auto rickshaw', - char: '\u{1F6FA}', - shortName: 'auto_rickshaw', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'uc12', - 'transportation', - 'car', - 'travel', - 'vacation', - 'thai', - 'chinese', - 'cart', - 'tuk tuk', - 'cars', - 'vehicle', - 'fast car', - 'drive', - 'driving', - 'auto', - 'pattaya', - 'chinois', - 'asian', - 'chine', - 'pedicab', - 'trishaw', - 'jinrikisha' - ]), - Emoji( - name: 'police car light', - char: '\u{1F6A8}', - shortName: 'rotating_light', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'beacon', - 'car', - 'light', - 'police', - 'revolving', - 'uc6', - 'transportation', - 'police', - '911', - 'sirens', - 'help', - 'cop', - 'policeman', - 'popo', - 'prison', - 'handcuff', - 'jail', - 'justice', - 'emergency', - 'injury', - 'switch' - ]), - Emoji( - name: 'oncoming police car', - char: '\u{1F694}', - shortName: 'oncoming_police_car', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'car', - 'oncoming', - 'police', - 'uc6', - 'transportation', - 'car', - 'police', - '911', - 'sirens', - 'help', - 'cars', - 'vehicle', - 'fast car', - 'drive', - 'driving', - 'auto', - 'cop', - 'policeman', - 'popo', - 'prison', - 'handcuff', - 'jail', - 'justice', - 'emergency', - 'injury', - 'switch' - ]), - Emoji( - name: 'oncoming bus', - char: '\u{1F68D}', - shortName: 'oncoming_bus', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'bus', - 'oncoming', - 'uc6', - 'transportation', - 'bus', - 'travel', - 'buses' - ]), - Emoji( - name: 'oncoming automobile', - char: '\u{1F698}', - shortName: 'oncoming_automobile', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'automobile', - 'car', - 'oncoming', - 'uc6', - 'transportation', - 'car', - 'travel', - 'cars', - 'vehicle', - 'fast car', - 'drive', - 'driving', - 'auto' - ]), - Emoji( - name: 'oncoming taxi', - char: '\u{1F696}', - shortName: 'oncoming_taxi', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'oncoming', - 'taxi', - 'uc6', - 'transportation', - 'car', - 'travel', - 'cars', - 'vehicle', - 'fast car', - 'drive', - 'driving', - 'auto' - ]), - Emoji( - name: 'aerial tramway', - char: '\u{1F6A1}', - shortName: 'aerial_tramway', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportAir, - keywords: [ - 'aerial', - 'cable', - 'car', - 'gondola', - 'tramway', - 'uc6', - 'transportation', - 'train', - 'travel', - 'disney', - 'trains', - 'cartoon' - ]), - Emoji( - name: 'mountain cableway', - char: '\u{1F6A0}', - shortName: 'mountain_cableway', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportAir, - keywords: [ - 'cable', - 'gondola', - 'mountain', - 'uc6', - 'transportation', - 'train', - 'travel', - 'skiing', - 'snowboarding', - 'trains', - 'ski', - 'snow skiing', - 'ski boot', - 'snowboarder' - ]), - Emoji( - name: 'suspension railway', - char: '\u{1F69F}', - shortName: 'suspension_railway', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportAir, - keywords: [ - 'railway', - 'suspension', - 'uc6', - 'transportation', - 'train', - 'travel', - 'trains' - ]), - Emoji( - name: 'railway car', - char: '\u{1F683}', - shortName: 'railway_car', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'car', - 'electric', - 'railway', - 'train', - 'tram', - 'trolleybus', - 'uc6', - 'transportation', - 'train', - 'travel', - 'trains' - ]), - Emoji( - name: 'tram car', - char: '\u{1F68B}', - shortName: 'train', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'car', - 'tram', - 'trolleybus', - 'uc6', - 'transportation', - 'train', - 'travel', - 'trains' - ]), - Emoji( - name: 'mountain railway', - char: '\u{1F69E}', - shortName: 'mountain_railway', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'car', - 'mountain', - 'railway', - 'uc6', - 'transportation', - 'train', - 'travel', - 'vacation', - 'mountain', - 'trains' - ]), - Emoji( - name: 'monorail', - char: '\u{1F69D}', - shortName: 'monorail', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'vehicle', - 'uc6', - 'transportation', - 'train', - 'travel', - 'vacation', - 'disney', - 'trains', - 'cartoon' - ]), - Emoji( - name: 'high-speed train', - char: '\u{1F684}', - shortName: 'bullettrain_side', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'railway', - 'shinkansen', - 'speed', - 'train', - 'uc6', - 'transportation', - 'train', - 'travel', - 'vacation', - 'trains' - ]), - Emoji( - name: 'bullet train', - char: '\u{1F685}', - shortName: 'bullettrain_front', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'bullet', - 'railway', - 'shinkansen', - 'speed', - 'train', - 'uc6', - 'transportation', - 'train', - 'travel', - 'vacation', - 'trains' - ]), - Emoji( - name: 'light rail', - char: '\u{1F688}', - shortName: 'light_rail', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'railway', - 'uc6', - 'transportation', - 'train', - 'travel', - 'trains' - ]), - Emoji( - name: 'locomotive', - char: '\u{1F682}', - shortName: 'steam_locomotive', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'engine', - 'railway', - 'steam', - 'train', - 'uc6', - 'transportation', - 'train', - 'travel', - 'steam', - 'disney', - 'trains', - 'steaming', - 'piping', - 'cartoon' - ]), - Emoji( - name: 'train', - char: '\u{1F686}', - shortName: 'train2', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'railway', - 'uc6', - 'transportation', - 'train', - 'travel', - 'trains' - ]), - Emoji( - name: 'metro', - char: '\u{1F687}', - shortName: 'metro', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'subway', - 'uc6', - 'transportation', - 'train', - 'travel', - 'trains' - ]), - Emoji( - name: 'tram', - char: '\u{1F68A}', - shortName: 'tram', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'trolleybus', - 'uc6', - 'transportation', - 'train', - 'travel', - 'trains' - ]), - Emoji( - name: 'station', - char: '\u{1F689}', - shortName: 'station', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'railway', - 'train', - 'uc6', - 'transportation', - 'train', - 'travel', - 'vacation', - 'trains' - ]), - Emoji( - name: 'airplane', - char: '\u{2708}\u{FE0F}', - shortName: 'airplane', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportAir, - keywords: [ - 'aeroplane', - 'airplane', - 'uc1', - 'transportation', - 'plane', - 'fly', - 'travel', - 'vacation', - 'airplane', - 'planes', - 'flight', - 'flying', - 'flights', - 'avion', - 'airline', - 'aircraft', - 'airforce', - 'air force', - 'airport' - ]), - Emoji( - name: 'airplane departure', - char: '\u{1F6EB}', - shortName: 'airplane_departure', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportAir, - keywords: [ - 'aeroplane', - 'airplane', - 'check-in', - 'departure', - 'departures', - 'uc7', - 'transportation', - 'plane', - 'fly', - 'travel', - 'vacation', - 'airplane', - 'planes', - 'flight', - 'flying', - 'flights', - 'avion', - 'airline', - 'aircraft', - 'airforce', - 'air force', - 'airport' - ]), - Emoji( - name: 'airplane arrival', - char: '\u{1F6EC}', - shortName: 'airplane_arriving', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportAir, - keywords: [ - 'aeroplane', - 'airplane', - 'arrivals', - 'arriving', - 'landing', - 'uc7', - 'transportation', - 'plane', - 'fly', - 'travel', - 'vacation', - 'airplane', - 'planes', - 'flight', - 'flying', - 'flights', - 'avion', - 'airline', - 'aircraft', - 'airforce', - 'air force', - 'airport' - ]), - Emoji( - name: 'small airplane', - char: '\u{1F6E9}\u{FE0F}', - shortName: 'airplane_small', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportAir, - keywords: [ - 'aeroplane', - 'airplane', - 'uc7', - 'transportation', - 'plane', - 'fly', - 'travel', - 'vacation', - 'airplane', - 'rich', - 'planes', - 'flight', - 'flying', - 'flights', - 'avion', - 'airline', - 'aircraft', - 'airforce', - 'air force', - 'airport', - 'grand', - 'expensive', - 'fancy' - ]), - Emoji( - name: 'seat', - char: '\u{1F4BA}', - shortName: 'seat', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportAir, - keywords: [ - 'chair', - 'uc6', - 'transportation', - 'fly', - 'travel', - 'vacation', - 'seat', - 'flight', - 'flying', - 'flights', - 'avion', - 'bench', - 'sedia', - 'Stuhl', - 'chaise', - 'silla', - 'armchair' - ]), - Emoji( - name: 'satellite', - char: '\u{1F6F0}\u{FE0F}', - shortName: 'satellite_orbital', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportAir, - keywords: [ - 'space', - 'uc7', - 'space', - 'drone', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship' - ]), - Emoji( - name: 'rocket', - char: '\u{1F680}', - shortName: 'rocket', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportAir, - keywords: [ - 'space', - 'uc6', - 'transportation', - 'fly', - 'space', - 'travel', - 'blast', - 'star wars', - 'flight', - 'flying', - 'flights', - 'avion', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'boom' - ]), - Emoji( - name: 'flying saucer', - char: '\u{1F6F8}', - shortName: 'flying_saucer', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportAir, - keywords: [ - 'UFO', - 'uc10', - 'transportation', - 'space', - 'travel', - 'alien', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'ufo' - ]), - Emoji( - name: 'helicopter', - char: '\u{1F681}', - shortName: 'helicopter', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportAir, - keywords: [ - 'vehicle', - 'uc6', - 'transportation', - 'plane', - 'fly', - 'travel', - 'vacation', - 'rich', - 'planes', - 'flight', - 'flying', - 'flights', - 'avion', - 'grand', - 'expensive', - 'fancy' - ]), - Emoji( - name: 'canoe', - char: '\u{1F6F6}', - shortName: 'canoe', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportWater, - keywords: [ - 'boat', - 'canoe', - 'uc9', - 'transportation', - 'travel', - 'rowing', - 'rowboat', - 'canoe' - ]), - Emoji( - name: 'sailboat', - char: '\u{26F5}', - shortName: 'sailboat', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportWater, - keywords: [ - 'boat', - 'resort', - 'sea', - 'yacht', - 'uc5', - 'transportation', - 'travel', - 'boat', - 'vacation', - 'pirate', - 'rich', - 'ocean', - 'boats', - 'boating', - 'grand', - 'expensive', - 'fancy', - 'sea' - ]), - Emoji( - name: 'speedboat', - char: '\u{1F6A4}', - shortName: 'speedboat', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportWater, - keywords: [ - 'boat', - 'uc6', - 'transportation', - 'travel', - 'boat', - 'tropical', - 'vacation', - 'florida', - 'scuba', - 'boats', - 'boating', - 'snorkel' - ]), - Emoji( - name: 'motor boat', - char: '\u{1F6E5}\u{FE0F}', - shortName: 'motorboat', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportWater, - keywords: [ - 'boat', - 'motorboat', - 'uc7', - 'transportation', - 'travel', - 'boat', - 'scuba', - 'rich', - 'boats', - 'boating', - 'snorkel', - 'grand', - 'expensive', - 'fancy' - ]), - Emoji( - name: 'passenger ship', - char: '\u{1F6F3}\u{FE0F}', - shortName: 'cruise_ship', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportWater, - keywords: [ - 'passenger', - 'ship', - 'uc7', - 'transportation', - 'travel', - 'boat', - 'vacation', - 'disney', - 'florida', - 'fun', - 'ocean', - 'boats', - 'boating', - 'cartoon', - 'sea' - ]), - Emoji( - name: 'ferry', - char: '\u{26F4}\u{FE0F}', - shortName: 'ferry', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportWater, - keywords: [ - 'boat', - 'passenger', - 'uc5', - 'transportation', - 'travel', - 'boat', - 'vacation', - 'ocean', - 'boats', - 'boating', - 'sea' - ]), - Emoji( - name: 'ship', - char: '\u{1F6A2}', - shortName: 'ship', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportWater, - keywords: [ - 'boat', - 'passenger', - 'uc6', - 'transportation', - 'travel', - 'boat', - 'smoking', - 'vacation', - 'moving', - 'ocean', - 'boats', - 'boating', - 'smoke', - 'cigarette', - 'puff', - 'sea' - ]), - Emoji( - name: 'anchor', - char: '\u{2693}', - shortName: 'anchor', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportWater, - keywords: [ - 'ship', - 'tool', - 'uc4', - 'boat', - 'vacation', - 'pirate', - 'boats', - 'boating' - ]), - Emoji( - name: 'fuel pump', - char: '\u{26FD}', - shortName: 'fuelpump', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'fuel', - 'fuelpump', - 'gas', - 'pump', - 'station', - 'uc5', - 'travel', - 'gas pump', - 'petrol' - ]), - Emoji( - name: 'construction', - char: '\u{1F6A7}', - shortName: 'construction', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: ['barrier', 'uc6', 'construction']), - Emoji( - name: 'vertical traffic light', - char: '\u{1F6A6}', - shortName: 'vertical_traffic_light', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: ['light', 'signal', 'traffic', 'uc6', 'stop light']), - Emoji( - name: 'horizontal traffic light', - char: '\u{1F6A5}', - shortName: 'traffic_light', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: ['light', 'signal', 'traffic', 'uc6', 'stop light']), - Emoji( - name: 'bus stop', - char: '\u{1F68F}', - shortName: 'busstop', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: ['bus', 'busstop', 'stop', 'uc6']), - Emoji( - name: 'world map', - char: '\u{1F5FA}\u{FE0F}', - shortName: 'map', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeMap, - keywords: [ - 'map', - 'world', - 'uc7', - 'places', - 'travel', - 'map', - 'vacation', - 'pirate', - 'history', - 'minecraft', - 'navigate', - 'direction', - 'world', - 'maps', - 'location', - 'locate', - 'local', - 'lost', - 'ancient', - 'old' - ]), - Emoji( - name: 'moai', - char: '\u{1F5FF}', - shortName: 'moyai', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.otherObject, - keywords: [ - 'face', - 'moyai', - 'statue', - 'uc6', - 'places', - 'travel', - 'japan', - 'vacation', - 'memorial', - 'japanese', - 'ninja' - ]), - Emoji( - name: 'Statue of Liberty', - char: '\u{1F5FD}', - shortName: 'statue_of_liberty', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'liberty', - 'statue', - 'uc6', - 'places', - 'america', - 'travel', - 'vacation', - 'statue of liberty', - 'free speech', - 'new york', - 'independence day', - 'memorial', - 'usa', - 'united states', - 'united states of america', - 'american', - 'statueofliberty', - 'freedom of speech', - '4th of july' - ]), - Emoji( - name: 'Tokyo tower', - char: '\u{1F5FC}', - shortName: 'tokyo_tower', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'Tokyo', - 'tower', - 'uc6', - 'building', - 'places', - 'travel', - 'japan', - 'vacation', - 'memorial', - 'buildings', - 'japanese', - 'ninja' - ]), - Emoji( - name: 'castle', - char: '\u{1F3F0}', - shortName: 'european_castle', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'European', - 'uc6', - 'building', - 'places', - 'travel', - 'vacation', - 'paris', - 'history', - 'irish', - 'scotland', - 'viking', - 'minecraft', - 'buildings', - 'french', - 'france', - 'ancient', - 'old', - 'saint patricks day', - 'st patricks day', - 'leprechaun', - 'scottish', - 'knight' - ]), - Emoji( - name: 'Japanese castle', - char: '\u{1F3EF}', - shortName: 'japanese_castle', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'Japanese', - 'castle', - 'uc6', - 'building', - 'places', - 'travel', - 'japan', - 'vacation', - 'buildings', - 'japanese', - 'ninja' - ]), - Emoji( - name: 'stadium', - char: '\u{1F3DF}\u{FE0F}', - shortName: 'stadium', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'stadium', - 'uc7', - 'building', - 'instruments', - 'places', - 'travel', - 'game', - 'vacation', - 'boys night', - 'buildings', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'games', - 'gaming', - 'guys night' - ]), - Emoji( - name: 'ferris wheel', - char: '\u{1F3A1}', - shortName: 'ferris_wheel', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeOther, - keywords: [ - 'amusement park', - 'ferris', - 'wheel', - 'uc6', - 'places', - 'travel', - 'vacation', - 'amusement park', - 'circus', - 'ferris wheel', - 'england', - 'disney', - 'fun', - 'summer', - 'independence day', - 'theme park', - 'circus tent', - 'clown', - 'clowns', - 'united kingdom', - 'london', - 'uk', - 'cartoon', - 'weekend', - '4th of july' - ]), - Emoji( - name: 'roller coaster', - char: '\u{1F3A2}', - shortName: 'roller_coaster', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeOther, - keywords: [ - 'amusement park', - 'coaster', - 'roller', - 'uc6', - 'places', - 'travel', - 'vacation', - 'amusement park', - 'disney', - 'fun', - 'summer', - 'theme park', - 'cartoon', - 'weekend' - ]), - Emoji( - name: 'carousel horse', - char: '\u{1F3A0}', - shortName: 'carousel_horse', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeOther, - keywords: [ - 'carousel', - 'horse', - 'uc6', - 'places', - 'vacation', - 'amusement park', - 'carousel', - 'donkey', - 'disney', - 'fun', - 'independence day', - 'theme park', - 'carousel horse', - 'poney', - 'cartoon', - '4th of july' - ]), - Emoji( - name: 'fountain', - char: '\u{26F2}', - shortName: 'fountain', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeOther, - keywords: [ - 'fountain', - 'uc5', - 'places', - 'travel', - 'vacation', - 'rich', - 'memorial', - 'grand', - 'expensive', - 'fancy' - ]), - Emoji( - name: 'umbrella on ground', - char: '\u{26F1}\u{FE0F}', - shortName: 'beach_umbrella', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'rain', - 'sun', - 'umbrella', - 'uc5', - 'travel', - 'tropical', - 'vacation', - 'umbrella', - 'hawaii', - 'california', - 'florida', - 'summer', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'weekend' - ]), - Emoji( - name: 'beach with umbrella', - char: '\u{1F3D6}\u{FE0F}', - shortName: 'beach', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeGeographic, - keywords: [ - 'beach', - 'umbrella', - 'uc7', - 'places', - 'travel', - 'tropical', - 'vacation', - 'swim', - 'beach', - 'australia', - 'umbrella', - 'hawaii', - 'california', - 'florida', - 'fun', - 'summer', - 'swimming', - 'swimmer', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'weekend' - ]), - Emoji( - name: 'desert island', - char: '\u{1F3DD}\u{FE0F}', - shortName: 'island', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeGeographic, - keywords: [ - 'desert', - 'island', - 'uc7', - 'places', - 'travel', - 'tropical', - 'vacation', - 'swim', - 'beach', - 'hawaii', - 'florida', - 'summer', - 'swimming', - 'swimmer', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'weekend' - ]), - Emoji( - name: 'desert', - char: '\u{1F3DC}\u{FE0F}', - shortName: 'desert', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeGeographic, - keywords: [ - 'desert', - 'uc7', - 'places', - 'travel', - 'vacation', - 'hot', - 'australia', - 'california', - 'las vegas', - 'heat', - 'warm', - 'caliente', - 'chaud', - 'heiß', - 'vegas' - ]), - Emoji( - name: 'volcano', - char: '\u{1F30B}', - shortName: 'volcano', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeGeographic, - keywords: [ - 'eruption', - 'mountain', - 'uc6', - 'places', - 'travel', - 'japan', - 'smoking', - 'tropical', - 'mountain', - 'explosion', - 'hawaii', - 'minecraft', - 'japanese', - 'ninja', - 'smoke', - 'cigarette', - 'puff', - 'explode', - 'aloha', - 'kawaii', - 'maui', - 'moana' - ]), - Emoji( - name: 'mountain', - char: '\u{26F0}\u{FE0F}', - shortName: 'mountain', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeGeographic, - keywords: [ - 'mountain', - 'uc5', - 'places', - 'travel', - 'camp', - 'vacation', - 'mountain', - 'climb', - 'camping', - 'tent', - 'camper', - 'outdoor', - 'outside' - ]), - Emoji( - name: 'snow-capped mountain', - char: '\u{1F3D4}\u{FE0F}', - shortName: 'mountain_snow', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeGeographic, - keywords: [ - 'cold', - 'mountain', - 'snow', - 'uc7', - 'places', - 'winter', - 'travel', - 'snow', - 'camp', - 'vacation', - 'cold', - 'snowboarding', - 'mountain', - 'paris', - 'polar bear', - 'freeze', - 'frozen', - 'frost', - 'ice cube', - 'camping', - 'tent', - 'camper', - 'outdoor', - 'outside', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles', - 'snowboarder', - 'french', - 'france' - ]), - Emoji( - name: 'mount fuji', - char: '\u{1F5FB}', - shortName: 'mount_fuji', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeGeographic, - keywords: [ - 'fuji', - 'mountain', - 'uc6', - 'places', - 'travel', - 'japan', - 'camp', - 'vacation', - 'cold', - 'mountain', - 'japanese', - 'ninja', - 'camping', - 'tent', - 'camper', - 'outdoor', - 'outside', - 'chilly', - 'chilled', - 'brisk', - 'freezing', - 'frostbite', - 'icicles' - ]), - Emoji( - name: 'camping', - char: '\u{1F3D5}\u{FE0F}', - shortName: 'camping', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeGeographic, - keywords: [ - 'camping', - 'uc7', - 'places', - 'travel', - 'camp', - 'vacation', - 'mountain', - 'fun', - 'parks', - 'camping', - 'tent', - 'camper', - 'outdoor', - 'outside', - 'regional park', - 'nature park', - 'natural park' - ]), - Emoji( - name: 'tent', - char: '\u{26FA}', - shortName: 'tent', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeOther, - keywords: [ - 'camping', - 'uc5', - 'places', - 'travel', - 'camp', - 'vacation', - 'summer', - 'camping', - 'tent', - 'camper', - 'outdoor', - 'outside', - 'weekend' - ]), - Emoji( - name: 'house', - char: '\u{1F3E0}', - shortName: 'house', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'home', - 'house', - 'uc6', - 'building', - 'places', - 'house', - 'covid', - 'buildings', - 'houses', - 'apartment', - 'apartments', - 'casa', - 'maison', - 'home' - ]), - Emoji( - name: 'house with garden', - char: '\u{1F3E1}', - shortName: 'house_with_garden', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'garden', - 'home', - 'house', - 'uc6', - 'building', - 'places', - 'house', - 'buildings', - 'houses', - 'apartment', - 'apartments', - 'casa', - 'maison', - 'home' - ]), - Emoji( - name: 'houses', - char: '\u{1F3D8}\u{FE0F}', - shortName: 'homes', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'houses', - 'uc7', - 'building', - 'places', - 'house', - 'buildings', - 'houses', - 'apartment', - 'apartments', - 'casa', - 'maison', - 'home' - ]), - Emoji( - name: 'derelict house', - char: '\u{1F3DA}\u{FE0F}', - shortName: 'house_abandoned', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'derelict', - 'house', - 'uc7', - 'building', - 'places', - 'house', - 'halloween', - 'buildings', - 'houses', - 'apartment', - 'apartments', - 'casa', - 'maison', - 'home', - 'samhain' - ]), - Emoji( - name: 'hut', - char: '\u{1F6D6}', - shortName: 'hut', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: ['uc13', 'places', 'star wars']), - Emoji( - name: 'building construction', - char: '\u{1F3D7}\u{FE0F}', - shortName: 'construction_site', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'construction', - 'uc7', - 'building', - 'crane', - 'build', - 'construction', - 'buildings' - ]), - Emoji( - name: 'factory', - char: '\u{1F3ED}', - shortName: 'factory', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'building', - 'uc6', - 'building', - 'places', - 'travel', - 'steam', - 'power', - 'poison', - 'buildings', - 'steaming', - 'piping', - 'toxic', - 'toxins' - ]), - Emoji( - name: 'office building', - char: '\u{1F3E2}', - shortName: 'office', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'building', - 'uc6', - 'building', - 'places', - 'classroom', - 'business', - 'work', - 'buildings', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'office' - ]), - Emoji( - name: 'department store', - char: '\u{1F3EC}', - shortName: 'department_store', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'department', - 'store', - 'uc6', - 'building', - 'places', - 'disney', - 'buildings', - 'cartoon' - ]), - Emoji( - name: 'Japanese post office', - char: '\u{1F3E3}', - shortName: 'post_office', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'Japanese', - 'post', - 'uc6', - 'building', - 'places', - 'japan', - 'mail', - 'buildings', - 'japanese', - 'ninja', - 'email', - 'post', - 'post office' - ]), - Emoji( - name: 'post office', - char: '\u{1F3E4}', - shortName: 'european_post_office', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'European', - 'post', - 'uc6', - 'building', - 'places', - 'mail', - 'buildings', - 'email', - 'post', - 'post office' - ]), - Emoji( - name: 'hospital', - char: '\u{1F3E5}', - shortName: 'hospital', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'doctor', - 'medicine', - 'uc6', - 'building', - 'places', - 'health', - '911', - 'nurse', - 'covid', - 'buildings', - 'medicine', - 'doctor', - 'emergency', - 'injury' - ]), - Emoji( - name: 'bank', - char: '\u{1F3E6}', - shortName: 'bank', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'building', - 'uc6', - 'building', - 'places', - 'money', - 'rich', - 'buildings', - 'cash', - 'dollars', - 'dollar', - 'bucks', - 'currency', - 'funds', - 'payment', - 'money face', - 'reward', - 'thief', - 'bank', - 'benjamins', - 'argent', - 'dinero', - 'i soldi', - 'Geld', - 'grand', - 'expensive', - 'fancy' - ]), - Emoji( - name: 'hotel', - char: '\u{1F3E8}', - shortName: 'hotel', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'building', - 'uc6', - 'building', - 'places', - 'vacation', - 'las vegas', - 'hotel', - 'buildings', - 'vegas', - 'vacancy', - 'no vacancy' - ]), - Emoji( - name: 'convenience store', - char: '\u{1F3EA}', - shortName: 'convenience_store', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'convenience', - 'store', - 'uc6', - 'building', - 'places', - 'las vegas', - 'buildings', - 'vegas' - ]), - Emoji( - name: 'school', - char: '\u{1F3EB}', - shortName: 'school', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'building', - 'uc6', - 'building', - 'places', - 'classroom', - 'buildings', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning' - ]), - Emoji( - name: 'love hotel', - char: '\u{1F3E9}', - shortName: 'love_hotel', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'hotel', - 'love', - 'uc6', - 'building', - 'places', - 'love', - 'japan', - 'vacation', - 'pink', - 'porn', - 'hotel', - 'buildings', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'japanese', - 'ninja', - 'rose', - 'vacancy', - 'no vacancy' - ]), - Emoji( - name: 'wedding', - char: '\u{1F492}', - shortName: 'wedding', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'chapel', - 'romance', - 'uc6', - 'building', - 'places', - 'wedding', - 'love', - 'pink', - 'buildings', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'rose' - ]), - Emoji( - name: 'classical building', - char: '\u{1F3DB}\u{FE0F}', - shortName: 'classical_building', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'classical', - 'uc7', - 'building', - 'places', - 'travel', - 'vacation', - 'police', - 'history', - 'court', - 'memorial', - 'buildings', - 'cop', - 'policeman', - 'popo', - 'prison', - 'handcuff', - 'jail', - 'justice', - 'ancient', - 'old' - ]), - Emoji( - name: 'church', - char: '\u{26EA}', - shortName: 'church', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeReligious, - keywords: [ - 'Christian', - 'cross', - 'religion', - 'uc5', - 'building', - 'places', - 'wedding', - 'religion', - 'travel', - 'christmas', - 'pray', - 'condolence', - 'jesus', - 'easter', - 'bible', - 'advent', - 'buildings', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'compassion' - ]), - Emoji( - name: 'mosque', - char: '\u{1F54C}', - shortName: 'mosque', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeReligious, - keywords: [ - 'Muslim', - 'islam', - 'religion', - 'uc8', - 'building', - 'places', - 'religion', - 'vacation', - 'pray', - 'condolence', - 'islam', - 'buildings', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'compassion', - 'muslim', - 'arab' - ]), - Emoji( - name: 'synagogue', - char: '\u{1F54D}', - shortName: 'synagogue', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeReligious, - keywords: [ - 'Jew', - 'Jewish', - 'religion', - 'temple', - 'uc8', - 'building', - 'places', - 'wedding', - 'religion', - 'vacation', - 'pray', - 'condolence', - 'jewish', - 'buildings', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'compassion', - 'hannukah', - 'hanukkah', - 'israel' - ]), - Emoji( - name: 'hindu temple', - char: '\u{1F6D5}', - shortName: 'hindu_temple', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeReligious, - keywords: [ - 'uc12', - 'building', - 'places', - 'travel', - 'pray', - 'dharmachakra', - 'buildings', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'jainism', - 'buddhism', - 'hinduism', - 'nirvana', - 'maintain', - 'keep', - 'law', - 'bueno', - 'dharma', - 'kama', - 'artha', - 'moksa', - 'karma' - ]), - Emoji( - name: 'kaaba', - char: '\u{1F54B}', - shortName: 'kaaba', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeReligious, - keywords: [ - 'Muslim', - 'islam', - 'religion', - 'uc8', - 'building', - 'places', - 'religion', - 'pray', - 'condolence', - 'islam', - 'buildings', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering', - 'compassion', - 'muslim', - 'arab' - ]), - Emoji( - name: 'shinto shrine', - char: '\u{26E9}\u{FE0F}', - shortName: 'shinto_shrine', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeReligious, - keywords: [ - 'religion', - 'shinto', - 'shrine', - 'uc5', - 'building', - 'places', - 'travel', - 'japan', - 'vacation', - 'buildings', - 'japanese', - 'ninja' - ]), - Emoji( - name: 'railway track', - char: '\u{1F6E4}\u{FE0F}', - shortName: 'railway_track', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'railway', - 'train', - 'uc7', - 'train', - 'travel', - 'vacation', - 'trains' - ]), - Emoji( - name: 'motorway', - char: '\u{1F6E3}\u{FE0F}', - shortName: 'motorway', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'highway', - 'road', - 'uc7', - 'travel', - 'camp', - 'vacation', - 'road', - 'camping', - 'tent', - 'camper', - 'outdoor', - 'outside', - 'route', - 'highway', - 'street' - ]), - Emoji( - name: 'map of Japan', - char: '\u{1F5FE}', - shortName: 'japan', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeMap, - keywords: [ - 'Japan', - 'map', - 'uc6', - 'places', - 'travel', - 'japan', - 'map', - 'vacation', - 'japanese', - 'ninja', - 'maps', - 'location', - 'locate', - 'local', - 'lost' - ]), - Emoji( - name: 'moon viewing ceremony', - char: '\u{1F391}', - shortName: 'rice_scene', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.event, - keywords: [ - 'celebration', - 'ceremony', - 'moon', - 'uc6', - 'places', - 'space', - 'sky', - 'travel', - 'japan', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'japanese', - 'ninja' - ]), - Emoji( - name: 'national park', - char: '\u{1F3DE}\u{FE0F}', - shortName: 'park', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeGeographic, - keywords: [ - 'park', - 'uc7', - 'places', - 'travel', - 'camp', - 'vacation', - 'summer', - 'river', - 'memorial', - 'parks', - 'camping', - 'tent', - 'camper', - 'outdoor', - 'outside', - 'weekend', - 'regional park', - 'nature park', - 'natural park' - ]), - Emoji( - name: 'sunrise', - char: '\u{1F305}', - shortName: 'sunrise', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeOther, - keywords: [ - 'morning', - 'sun', - 'uc6', - 'places', - 'sun', - 'sky', - 'travel', - 'tropical', - 'vacation', - 'day', - 'hump day', - 'morning', - 'hawaii', - 'california', - 'florida', - 'scuba', - 'summer', - 'sunshine', - 'sunny', - 'eclipse', - 'solar', - 'solareclipse', - 'shiny', - 'good morning', - 'aloha', - 'kawaii', - 'maui', - 'moana', - 'snorkel', - 'weekend' - ]), - Emoji( - name: 'sunrise over mountains', - char: '\u{1F304}', - shortName: 'sunrise_over_mountains', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeOther, - keywords: [ - 'morning', - 'mountain', - 'sun', - 'sunrise', - 'uc6', - 'places', - 'sun', - 'sky', - 'travel', - 'camp', - 'vacation', - 'day', - 'morning', - 'mountain', - 'california', - 'sunshine', - 'sunny', - 'eclipse', - 'solar', - 'solareclipse', - 'shiny', - 'camping', - 'tent', - 'camper', - 'outdoor', - 'outside', - 'good morning' - ]), - Emoji( - name: 'shooting star', - char: '\u{1F320}', - shortName: 'stars', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'falling', - 'shooting', - 'star', - 'uc6', - 'space', - 'star wars', - 'fame', - 'sparkle', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'famous', - 'celebrity', - 'bright', - 'shine', - 'twinkle' - ]), - Emoji( - name: 'sparkler', - char: '\u{1F387}', - shortName: 'sparkler', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.event, - keywords: [ - 'celebration', - 'fireworks', - 'sparkle', - 'uc6', - 'holidays', - 'happy birthday', - 'firework', - 'explosion', - 'celebrate', - 'glitter', - 'disney', - 'sparkle', - 'holiday', - 'Bon anniversaire', - 'joyeux anniversaire', - 'buon compleanno', - 'feliz cumpleaños', - 'alles Gute zum Geburtstag', - 'feliz Aniversário', - 'Gratulerer med dagen', - 'fireworks', - 'firecracker', - 'explode', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'cartoon', - 'bright', - 'shine', - 'twinkle' - ]), - Emoji( - name: 'fireworks', - char: '\u{1F386}', - shortName: 'fireworks', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.event, - keywords: [ - 'celebration', - 'uc6', - 'holidays', - 'firework', - 'explosion', - 'celebrate', - 'glitter', - 'disney', - 'sparkle', - 'independence day', - 'holiday', - 'fireworks', - 'firecracker', - 'explode', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'cartoon', - 'bright', - 'shine', - 'twinkle', - '4th of july' - ]), - Emoji( - name: 'sunset', - char: '\u{1F307}', - shortName: 'city_sunset', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeOther, - keywords: [ - 'dusk', - 'sun', - 'uc6', - 'building', - 'places', - 'sky', - 'vacation', - 'buildings' - ]), - Emoji( - name: 'cityscape at dusk', - char: '\u{1F306}', - shortName: 'city_dusk', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeOther, - keywords: [ - 'city', - 'dusk', - 'evening', - 'landscape', - 'sun', - 'sunset', - 'uc6', - 'building', - 'places', - 'buildings' - ]), - Emoji( - name: 'cityscape', - char: '\u{1F3D9}\u{FE0F}', - shortName: 'cityscape', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeOther, - keywords: [ - 'city', - 'uc7', - 'building', - 'places', - 'vacation', - 'new york', - 'england', - 'donald trump', - 'rich', - 'buildings', - 'united kingdom', - 'london', - 'uk', - 'trump', - 'grand', - 'expensive', - 'fancy' - ]), - Emoji( - name: 'night with stars', - char: '\u{1F303}', - shortName: 'night_with_stars', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeOther, - keywords: [ - 'night', - 'star', - 'uc6', - 'building', - 'places', - 'halloween', - 'sky', - 'vacation', - 'goodnight', - 'buildings', - 'samhain' - ]), - Emoji( - name: 'milky way', - char: '\u{1F30C}', - shortName: 'milky_way', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'space', - 'uc6', - 'places', - 'space', - 'sky', - 'travel', - 'star', - 'vacation', - 'goodnight', - 'star wars', - 'dream', - 'fantasy', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'stars', - 'dreams' - ]), - Emoji( - name: 'bridge at night', - char: '\u{1F309}', - shortName: 'bridge_at_night', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeOther, - keywords: [ - 'bridge', - 'night', - 'uc6', - 'places', - 'travel', - 'vacation', - 'goodnight', - 'england', - 'california', - 'united kingdom', - 'london', - 'uk' - ]), - Emoji( - name: 'foggy', - char: '\u{1F301}', - shortName: 'foggy', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeOther, - keywords: [ - 'fog', - 'uc6', - 'building', - 'places', - 'sky', - 'travel', - 'vacation', - 'england', - 'buildings', - 'united kingdom', - 'london', - 'uk' - ]), - Emoji( - name: 'watch', - char: '\u{231A}', - shortName: 'watch', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.time, - keywords: [ - 'clock', - 'uc1', - 'electronics', - 'time', - 'accessories', - 'bling', - 'wait', - 'clocks', - 'clock', - 'jewels', - 'gems', - 'jewel', - 'jewelry', - 'treasure', - 'hours' - ]), - Emoji( - name: 'mobile phone', - char: '\u{1F4F1}', - shortName: 'mobile_phone', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.phone, - keywords: [ - 'cell', - 'mobile', - 'phone', - 'telephone', - 'uc6', - 'electronics', - 'phone', - 'talk', - 'selfie', - 'technology', - 'laptop', - 'instagram', - 'telephone', - 'iphone', - 'smartphone', - 'text', - 'talking', - 'speech', - 'social', - 'chat', - 'voice', - 'speechless', - 'speak', - 'computer', - 'online', - 'wifi', - 'website', - 'zoom', - 'ipad', - 'tablet' - ]), - Emoji( - name: 'mobile phone with arrow', - char: '\u{1F4F2}', - shortName: 'calling', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.phone, - keywords: [ - 'arrow', - 'call', - 'cell', - 'mobile', - 'phone', - 'receive', - 'telephone', - 'uc6', - 'electronics', - 'phone', - 'selfie', - 'technology', - 'download', - 'telephone', - 'iphone', - 'smartphone', - 'text' - ]), - Emoji( - name: 'laptop', - char: '\u{1F4BB}', - shortName: 'computer', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.computer, - keywords: [ - 'computer', - 'pc', - 'personal', - 'uc6', - 'electronics', - 'classroom', - 'internet', - 'technology', - 'laptop', - 'download', - 'instagram', - 'youtube', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'computer', - 'online', - 'wifi', - 'website', - 'zoom', - 'ipad', - 'tablet', - 'vlog', - 'office' - ]), - Emoji( - name: 'keyboard', - char: '\u{2328}\u{FE0F}', - shortName: 'keyboard', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.computer, - keywords: [ - 'computer', - 'uc1', - 'electronics', - 'classroom', - 'technology', - 'laptop', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'computer', - 'online', - 'wifi', - 'website', - 'zoom', - 'ipad', - 'tablet', - 'office' - ]), - Emoji( - name: 'desktop computer', - char: '\u{1F5A5}\u{FE0F}', - shortName: 'desktop', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.computer, - keywords: [ - 'computer', - 'desktop', - 'uc7', - 'electronics', - 'classroom', - 'internet', - 'technology', - 'laptop', - 'download', - 'instagram', - 'youtube', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'computer', - 'online', - 'wifi', - 'website', - 'zoom', - 'ipad', - 'tablet', - 'vlog', - 'office' - ]), - Emoji( - name: 'printer', - char: '\u{1F5A8}\u{FE0F}', - shortName: 'printer', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.computer, - keywords: [ - 'computer', - 'uc7', - 'electronics', - 'classroom', - 'technology', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'office' - ]), - Emoji( - name: 'computer mouse', - char: '\u{1F5B1}\u{FE0F}', - shortName: 'mouse_three_button', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.computer, - keywords: [ - 'computer', - 'uc7', - 'electronics', - 'classroom', - 'game', - 'technology', - 'laptop', - 'click', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'games', - 'gaming', - 'computer', - 'online', - 'wifi', - 'website', - 'zoom', - 'ipad', - 'tablet', - 'office' - ]), - Emoji( - name: 'trackball', - char: '\u{1F5B2}\u{FE0F}', - shortName: 'trackball', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.computer, - keywords: [ - 'computer', - 'uc7', - 'electronics', - 'classroom', - 'game', - 'technology', - 'laptop', - 'click', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'games', - 'gaming', - 'computer', - 'online', - 'wifi', - 'website', - 'zoom', - 'ipad', - 'tablet', - 'office' - ]), - Emoji( - name: 'joystick', - char: '\u{1F579}\u{FE0F}', - shortName: 'joystick', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.game, - keywords: [ - 'game', - 'video game', - 'uc7', - 'electronics', - 'game', - 'boys night', - 'technology', - 'controller', - 'pacman', - 'games', - 'gaming', - 'guys night', - 'remote', - 'pac man' - ]), - Emoji( - name: 'clamp', - char: '\u{1F5DC}\u{FE0F}', - shortName: 'compression', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.tool, - keywords: [ - 'compress', - 'tool', - 'vice', - 'uc7', - 'tool', - 'download', - 'steel', - 'tools', - 'metal' - ]), - Emoji( - name: 'computer disk', - char: '\u{1F4BD}', - shortName: 'minidisc', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.computer, - keywords: [ - 'computer', - 'disk', - 'minidisk', - 'optical', - 'uc6', - 'instruments', - 'electronics', - 'classroom', - 'laptop', - 'download', - 'work', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'computer', - 'online', - 'wifi', - 'website', - 'zoom', - 'ipad', - 'tablet', - 'office' - ]), - Emoji( - name: 'floppy disk', - char: '\u{1F4BE}', - shortName: 'floppy_disk', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.computer, - keywords: [ - 'computer', - 'disk', - 'floppy', - 'uc6', - 'electronics', - 'classroom', - 'laptop', - 'download', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'computer', - 'online', - 'wifi', - 'website', - 'zoom', - 'ipad', - 'tablet', - 'office' - ]), - Emoji( - name: 'optical disk', - char: '\u{1F4BF}', - shortName: 'cd', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.computer, - keywords: [ - 'cd', - 'computer', - 'disk', - 'optical', - 'uc6', - 'instruments', - 'electronics', - 'download', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique' - ]), - Emoji( - name: 'dvd', - char: '\u{1F4C0}', - shortName: 'dvd', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.computer, - keywords: [ - 'blu-ray', - 'computer', - 'disk', - 'dvd', - 'optical', - 'uc6', - 'electronics' - ]), - Emoji( - name: 'videocassette', - char: '\u{1F4FC}', - shortName: 'vhs', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.lightVideo, - keywords: [ - 'tape', - 'vhs', - 'video', - 'uc6', - 'electronics', - 'history', - 'ancient', - 'old' - ]), - Emoji( - name: 'camera', - char: '\u{1F4F7}', - shortName: 'camera', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.lightVideo, - keywords: [ - 'video', - 'uc6', - 'electronics', - 'selfie', - 'technology', - 'detective', - 'instagram', - 'youtube', - 'vlog' - ]), - Emoji( - name: 'camera with flash', - char: '\u{1F4F8}', - shortName: 'camera_with_flash', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.lightVideo, - keywords: [ - 'camera', - 'flash', - 'video', - 'uc7', - 'electronics', - 'technology', - 'instagram' - ]), - Emoji( - name: 'video camera', - char: '\u{1F4F9}', - shortName: 'video_camera', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.lightVideo, - keywords: [ - 'camera', - 'video', - 'uc6', - 'electronics', - 'movie', - 'technology', - 'porn', - 'youtube', - 'movies', - 'cinema', - 'film', - 'films', - 'video', - 'videos', - 'vlog' - ]), - Emoji( - name: 'movie camera', - char: '\u{1F3A5}', - shortName: 'movie_camera', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.lightVideo, - keywords: [ - 'camera', - 'cinema', - 'movie', - 'uc6', - 'movie', - 'movies', - 'cinema', - 'film', - 'films', - 'video', - 'videos' - ]), - Emoji( - name: 'film projector', - char: '\u{1F4FD}\u{FE0F}', - shortName: 'projector', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.lightVideo, - keywords: [ - 'cinema', - 'film', - 'movie', - 'projector', - 'video', - 'uc7', - 'movie', - 'disney', - 'california', - 'fame', - 'movies', - 'cinema', - 'film', - 'films', - 'video', - 'videos', - 'cartoon', - 'famous', - 'celebrity' - ]), - Emoji( - name: 'film frames', - char: '\u{1F39E}\u{FE0F}', - shortName: 'film_frames', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.lightVideo, - keywords: [ - 'cinema', - 'film', - 'frames', - 'movie', - 'uc7', - 'movie', - 'disney', - 'california', - 'movies', - 'cinema', - 'film', - 'films', - 'video', - 'videos', - 'cartoon' - ]), - Emoji( - name: 'telephone receiver', - char: '\u{1F4DE}', - shortName: 'telephone_receiver', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.phone, - keywords: [ - 'phone', - 'receiver', - 'telephone', - 'uc6', - 'electronics', - 'phone', - 'talk', - 'telephone', - 'iphone', - 'smartphone', - 'text', - 'talking', - 'speech', - 'social', - 'chat', - 'voice', - 'speechless', - 'speak' - ]), - Emoji( - name: 'telephone', - char: '\u{260E}\u{FE0F}', - shortName: 'telephone', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.phone, - keywords: [ - 'phone', - 'uc1', - 'electronics', - 'phone', - 'talk', - 'history', - 'hotel', - 'telephone', - 'iphone', - 'smartphone', - 'text', - 'talking', - 'speech', - 'social', - 'chat', - 'voice', - 'speechless', - 'speak', - 'ancient', - 'old', - 'vacancy', - 'no vacancy' - ]), - Emoji( - name: 'pager', - char: '\u{1F4DF}', - shortName: 'pager', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.phone, - keywords: [ - 'pager', - 'uc6', - 'electronics', - 'technology', - 'history', - 'work', - 'ancient', - 'old', - 'office' - ]), - Emoji( - name: 'fax machine', - char: '\u{1F4E0}', - shortName: 'fax', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.phone, - keywords: [ - 'fax', - 'uc6', - 'electronics', - 'classroom', - 'technology', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'office' - ]), - Emoji( - name: 'television', - char: '\u{1F4FA}', - shortName: 'tv', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.lightVideo, - keywords: [ - 'tv', - 'video', - 'uc6', - 'electronics', - 'classroom', - 'technology', - 'news', - 'fame', - 'history', - 'youtube', - 'household', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'article', - 'famous', - 'celebrity', - 'ancient', - 'old', - 'vlog' - ]), - Emoji( - name: 'radio', - char: '\u{1F4FB}', - shortName: 'radio', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.music, - keywords: [ - 'video', - 'uc6', - 'instruments', - 'electronics', - 'news', - 'history', - 'sound', - 'household', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'article', - 'ancient', - 'old', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear' - ]), - Emoji( - name: 'studio microphone', - char: '\u{1F399}\u{FE0F}', - shortName: 'microphone2', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.music, - keywords: [ - 'mic', - 'microphone', - 'music', - 'studio', - 'uc7', - 'instruments', - 'electronics', - 'news', - 'sound', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'article', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear' - ]), - Emoji( - name: 'level slider', - char: '\u{1F39A}\u{FE0F}', - shortName: 'level_slider', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.music, - keywords: [ - 'level', - 'music', - 'slider', - 'uc7', - 'instruments', - 'electronics', - 'sound', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear' - ]), - Emoji( - name: 'control knobs', - char: '\u{1F39B}\u{FE0F}', - shortName: 'control_knobs', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.music, - keywords: [ - 'control', - 'knobs', - 'music', - 'uc7', - 'instruments', - 'electronics', - 'power', - 'bake', - 'sound', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique', - 'baking', - 'volume', - 'speaker', - 'loud', - 'mic', - 'audio', - 'hear' - ]), - Emoji( - name: 'compass', - char: '\u{1F9ED}', - shortName: 'compass', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeMap, - keywords: [ - 'uc11', - 'travel', - 'camp', - 'science', - 'map', - 'globe', - 'navigate', - 'direction', - 'camping', - 'tent', - 'camper', - 'outdoor', - 'outside', - 'lab', - 'maps', - 'location', - 'locate', - 'local', - 'lost', - 'globes', - 'planet', - 'earth', - 'earthquake' - ]), - Emoji( - name: 'stopwatch', - char: '\u{23F1}\u{FE0F}', - shortName: 'stopwatch', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.time, - keywords: [ - 'clock', - 'uc6', - 'electronics', - 'time', - 'wait', - 'clocks', - 'clock', - 'hours' - ]), - Emoji( - name: 'timer clock', - char: '\u{23F2}\u{FE0F}', - shortName: 'timer', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.time, - keywords: [ - 'clock', - 'timer', - 'uc6', - 'time', - 'bake', - 'wait', - 'measure', - 'clocks', - 'clock', - 'baking', - 'hours' - ]), - Emoji( - name: 'alarm clock', - char: '\u{23F0}', - shortName: 'alarm_clock', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.time, - keywords: [ - 'alarm', - 'clock', - 'uc6', - 'time', - 'alarm', - 'wait', - 'clocks', - 'clock', - 'alarms', - 'announce', - 'hours' - ]), - Emoji( - name: 'mantelpiece clock', - char: '\u{1F570}\u{FE0F}', - shortName: 'clock', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.time, - keywords: [ - 'clock', - 'uc7', - 'time', - 'vintage', - 'wait', - 'household', - 'clocks', - 'clock', - 'hours' - ]), - Emoji( - name: 'hourglass done', - char: '\u{231B}', - shortName: 'hourglass', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.time, - keywords: [ - 'sand', - 'timer', - 'uc1', - 'time', - 'empty', - 'percent', - 'wait', - 'measure', - 'clocks', - 'clock', - 'hours' - ]), - Emoji( - name: 'hourglass not done', - char: '\u{23F3}', - shortName: 'hourglass_flowing_sand', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.time, - keywords: [ - 'hourglass', - 'sand', - 'timer', - 'uc6', - 'time', - 'infinity', - 'history', - 'percent', - 'wait', - 'measure', - 'clocks', - 'clock', - 'infini', - 'forever', - 'ancient', - 'old', - 'hours' - ]), - Emoji( - name: 'satellite antenna', - char: '\u{1F4E1}', - shortName: 'satellite', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.science, - keywords: ['antenna', 'dish', 'satellite', 'uc6', 'technology', 'power']), - Emoji( - name: 'battery', - char: '\u{1F50B}', - shortName: 'battery', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.computer, - keywords: ['battery', 'uc6', 'science', 'power', 'energy', 'lab']), - Emoji( - name: 'electric plug', - char: '\u{1F50C}', - shortName: 'electric_plug', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.computer, - keywords: [ - 'electric', - 'electricity', - 'plug', - 'uc6', - 'electronics', - 'electric', - 'power', - 'household', - 'energy' - ]), - Emoji( - name: 'light bulb', - char: '\u{1F4A1}', - shortName: 'bulb', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.lightVideo, - keywords: [ - 'bulb', - 'comic', - 'electric', - 'idea', - 'light', - 'uc6', - 'science', - 'electric', - 'light', - 'power', - 'idea', - 'sparkle', - 'innovate', - 'household', - 'energy', - 'lab', - 'lamp', - 'light bulb', - 'flashlight', - 'spotlight', - 'illuminate', - 'lightbulb', - 'lighting', - 'luce', - 'licht', - 'lumière', - 'luz', - 'bright', - 'shine', - 'twinkle', - 'innovation', - 'inquire' - ]), - Emoji( - name: 'flashlight', - char: '\u{1F526}', - shortName: 'flashlight', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.lightVideo, - keywords: [ - 'electric', - 'light', - 'tool', - 'torch', - 'uc6', - 'electronics', - 'tool', - 'light', - 'star wars', - 'search', - 'detective', - 'sparkle', - 'household', - 'energy', - 'tools', - 'lamp', - 'light bulb', - 'flashlight', - 'spotlight', - 'illuminate', - 'lightbulb', - 'lighting', - 'luce', - 'licht', - 'lumière', - 'luz', - 'look', - 'find', - 'looking', - 'see', - 'bright', - 'shine', - 'twinkle' - ]), - Emoji( - name: 'candle', - char: '\u{1F56F}\u{FE0F}', - shortName: 'candle', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.lightVideo, - keywords: [ - 'light', - 'uc7', - 'religion', - 'halloween', - 'birthday', - 'christmas', - 'light', - 'jewish', - 'samhain', - 'birth', - 'cumpleaños', - 'anniversaire', - 'bday', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'lamp', - 'light bulb', - 'flashlight', - 'spotlight', - 'illuminate', - 'lightbulb', - 'lighting', - 'luce', - 'licht', - 'lumière', - 'luz', - 'hannukah', - 'hanukkah', - 'israel' - ]), - Emoji( - name: 'diya lamp', - char: '\u{1FA94}', - shortName: 'diya_lamp', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.lightVideo, - keywords: [ - 'uc12', - 'light', - 'jealous', - 'dharmachakra', - 'diya', - 'greed', - 'soul', - 'lamp', - 'light bulb', - 'flashlight', - 'spotlight', - 'illuminate', - 'lightbulb', - 'lighting', - 'luce', - 'licht', - 'lumière', - 'luz', - 'jainism', - 'buddhism', - 'hinduism', - 'nirvana', - 'maintain', - 'keep', - 'law', - 'bueno', - 'dharma', - 'kama', - 'artha', - 'moksa', - 'karma', - 'diyo', - 'deya', - 'divaa', - 'deepa', - 'deepam', - 'deepak', - 'diwali', - 'oil lamp', - 'selfish' - ]), - Emoji( - name: 'fire extinguisher', - char: '\u{1F9EF}', - shortName: 'fire_extinguisher', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.household, - keywords: [ - 'uc11', - 'alarm', - 'science', - 'danger', - 'household', - 'fires', - 'alarms', - 'announce', - 'lab', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous' - ]), - Emoji( - name: 'oil drum', - char: '\u{1F6E2}\u{FE0F}', - shortName: 'oil', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.transportGround, - keywords: [ - 'drum', - 'oil', - 'uc7', - 'jewish', - 'hannukah', - 'hanukkah', - 'israel' - ]), - Emoji( - name: 'money with wings', - char: '\u{1F4B8}', - shortName: 'money_with_wings', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.money, - keywords: [ - 'bank', - 'banknote', - 'bill', - 'dollar', - 'fly', - 'money', - 'note', - 'wings', - 'uc6', - 'money', - 'vacation', - 'boys night', - 'coins', - 'las vegas', - 'rich', - 'purchase', - 'cash', - 'dollars', - 'dollar', - 'bucks', - 'currency', - 'funds', - 'payment', - 'money face', - 'reward', - 'thief', - 'bank', - 'benjamins', - 'argent', - 'dinero', - 'i soldi', - 'Geld', - 'guys night', - 'vegas', - 'grand', - 'expensive', - 'fancy', - 'buy', - 'shop', - 'spend' - ]), - Emoji( - name: 'dollar banknote', - char: '\u{1F4B5}', - shortName: 'dollar', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.money, - keywords: [ - 'bank', - 'banknote', - 'bill', - 'currency', - 'dollar', - 'money', - 'note', - 'uc6', - 'money', - 'coins', - 'rich', - 'purchase', - 'cash', - 'dollars', - 'dollar', - 'bucks', - 'currency', - 'funds', - 'payment', - 'money face', - 'reward', - 'thief', - 'bank', - 'benjamins', - 'argent', - 'dinero', - 'i soldi', - 'Geld', - 'grand', - 'expensive', - 'fancy', - 'buy', - 'shop', - 'spend' - ]), - Emoji( - name: 'yen banknote', - char: '\u{1F4B4}', - shortName: 'yen', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.money, - keywords: [ - 'bank', - 'banknote', - 'bill', - 'currency', - 'money', - 'note', - 'yen', - 'uc6', - 'money', - 'coins', - 'rich', - 'purchase', - 'cash', - 'dollars', - 'dollar', - 'bucks', - 'currency', - 'funds', - 'payment', - 'money face', - 'reward', - 'thief', - 'bank', - 'benjamins', - 'argent', - 'dinero', - 'i soldi', - 'Geld', - 'grand', - 'expensive', - 'fancy', - 'buy', - 'shop', - 'spend' - ]), - Emoji( - name: 'euro banknote', - char: '\u{1F4B6}', - shortName: 'euro', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.money, - keywords: [ - 'bank', - 'banknote', - 'bill', - 'currency', - 'euro', - 'money', - 'note', - 'uc6', - 'money', - 'coins', - 'rich', - 'purchase', - 'cash', - 'dollars', - 'dollar', - 'bucks', - 'currency', - 'funds', - 'payment', - 'money face', - 'reward', - 'thief', - 'bank', - 'benjamins', - 'argent', - 'dinero', - 'i soldi', - 'Geld', - 'grand', - 'expensive', - 'fancy', - 'buy', - 'shop', - 'spend' - ]), - Emoji( - name: 'pound banknote', - char: '\u{1F4B7}', - shortName: 'pound', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.money, - keywords: [ - 'bank', - 'banknote', - 'bill', - 'currency', - 'money', - 'note', - 'pound', - 'uc6', - 'money', - 'coins', - 'rich', - 'purchase', - 'cash', - 'dollars', - 'dollar', - 'bucks', - 'currency', - 'funds', - 'payment', - 'money face', - 'reward', - 'thief', - 'bank', - 'benjamins', - 'argent', - 'dinero', - 'i soldi', - 'Geld', - 'grand', - 'expensive', - 'fancy', - 'buy', - 'shop', - 'spend' - ]), - Emoji( - name: 'coin', - char: '\u{1FA99}', - shortName: 'coin', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.money, - keywords: [ - 'uc13', - 'money', - 'coins', - 'purchase', - 'cash', - 'dollars', - 'dollar', - 'bucks', - 'currency', - 'funds', - 'payment', - 'money face', - 'reward', - 'thief', - 'bank', - 'benjamins', - 'argent', - 'dinero', - 'i soldi', - 'Geld', - 'buy', - 'shop', - 'spend' - ]), - Emoji( - name: 'money bag', - char: '\u{1F4B0}', - shortName: 'moneybag', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.money, - keywords: [ - 'bag', - 'dollar', - 'money', - 'moneybag', - 'uc6', - 'wedding', - 'bag', - 'money', - 'award', - 'pirate', - 'bling', - 'coins', - 'donald trump', - 'rich', - 'weddings', - 'marriage', - 'newlywed', - 'bride', - 'groome', - 'groom', - 'married', - 'marry', - 'swag', - 'cash', - 'dollars', - 'dollar', - 'bucks', - 'currency', - 'funds', - 'payment', - 'money face', - 'reward', - 'thief', - 'bank', - 'benjamins', - 'argent', - 'dinero', - 'i soldi', - 'Geld', - 'awards', - 'prize', - 'prizes', - 'trophy', - 'trophies', - 'spot', - 'best', - 'champion', - 'hero', - 'jewels', - 'gems', - 'jewel', - 'jewelry', - 'treasure', - 'trump', - 'grand', - 'expensive', - 'fancy' - ]), - Emoji( - name: 'credit card', - char: '\u{1F4B3}', - shortName: 'credit_card', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.money, - keywords: [ - 'bank', - 'card', - 'credit', - 'money', - 'uc6', - 'money', - 'vacation', - 'boys night', - 'rich', - 'purchase', - 'cash', - 'dollars', - 'dollar', - 'bucks', - 'currency', - 'funds', - 'payment', - 'money face', - 'reward', - 'thief', - 'bank', - 'benjamins', - 'argent', - 'dinero', - 'i soldi', - 'Geld', - 'guys night', - 'grand', - 'expensive', - 'fancy', - 'buy', - 'shop', - 'spend' - ]), - Emoji( - name: 'gem stone', - char: '\u{1F48E}', - shortName: 'gem', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'diamond', - 'gem', - 'jewel', - 'uc6', - 'bling', - 'minecraft', - 'diamond', - 'rich', - 'sparkle', - 'jewels', - 'gems', - 'jewel', - 'jewelry', - 'treasure', - 'grand', - 'expensive', - 'fancy', - 'bright', - 'shine', - 'twinkle' - ]), - Emoji( - name: 'balance scale', - char: '\u{2696}\u{FE0F}', - shortName: 'scales', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.tool, - keywords: [ - 'Libra', - 'balance', - 'justice', - 'scales', - 'tool', - 'weight', - 'zodiac', - 'uc4', - 'tool', - 'science', - 'poison', - 'measure', - 'tools', - 'lab', - 'toxic', - 'toxins' - ]), - Emoji( - name: 'ladder', - char: '\u{1FA9C}', - shortName: 'ladder', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.tool, - keywords: ['uc13', 'tool', 'household', 'climb', 'tools']), - Emoji( - name: 'toolbox', - char: '\u{1F9F0}', - shortName: 'toolbox', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.tool, - keywords: ['uc11', 'tool', 'household', 'build', 'tools']), - Emoji( - name: 'screwdriver', - char: '\u{1FA9B}', - shortName: 'screwdriver', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.tool, - keywords: [ - 'uc13', - 'tool', - 'household', - 'build', - 'phillips', - 'tools', - 'flat tip', - 'flat head', - 'spiral ratchet', - 'ratchet', - 'slot head', - 'torx', - 'star head', - 'hex key' - ]), - Emoji( - name: 'wrench', - char: '\u{1F527}', - shortName: 'wrench', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.tool, - keywords: [ - 'spanner', - 'tool', - 'wrench', - 'uc6', - 'tool', - 'steel', - 'build', - 'tools', - 'metal' - ]), - Emoji( - name: 'hammer', - char: '\u{1F528}', - shortName: 'hammer', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.tool, - keywords: [ - 'tool', - 'uc6', - 'tool', - 'weapon', - 'steel', - 'household', - 'build', - 'tools', - 'weapons', - 'metal' - ]), - Emoji( - name: 'hammer and pick', - char: '\u{2692}\u{FE0F}', - shortName: 'hammer_pick', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.tool, - keywords: [ - 'hammer', - 'pick', - 'tool', - 'uc4', - 'tool', - 'weapon', - 'minecraft', - 'steel', - 'build', - 'chop', - 'tools', - 'weapons', - 'metal' - ]), - Emoji( - name: 'hammer and wrench', - char: '\u{1F6E0}\u{FE0F}', - shortName: 'tools', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.tool, - keywords: [ - 'hammer', - 'spanner', - 'tool', - 'wrench', - 'uc7', - 'tool', - 'steel', - 'build', - 'tools', - 'metal' - ]), - Emoji( - name: 'pick', - char: '\u{26CF}\u{FE0F}', - shortName: 'pick', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.tool, - keywords: [ - 'mining', - 'tool', - 'uc5', - 'tool', - 'weapon', - 'farm', - 'viking', - 'minecraft', - 'killer', - 'steel', - 'build', - 'chop', - 'shinobi', - 'tools', - 'weapons', - 'knight', - 'savage', - 'scary clown', - 'metal', - 'samurai' - ]), - Emoji( - name: 'nut and bolt', - char: '\u{1F529}', - shortName: 'nut_and_bolt', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.tool, - keywords: [ - 'bolt', - 'nut', - 'tool', - 'uc6', - 'tool', - 'nutcase', - 'steel', - 'build', - 'tools', - 'metal' - ]), - Emoji( - name: 'gear', - char: '\u{2699}\u{FE0F}', - shortName: 'gear', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.tool, - keywords: ['tool', 'uc4', 'tool', 'steel', 'tools', 'metal']), - Emoji( - name: 'brick', - char: '\u{1F9F1}', - shortName: 'bricks', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeBuilding, - keywords: [ - 'uc11', - 'house', - 'trap', - 'donald trump', - 'private', - 'household', - 'build', - 'block', - 'houses', - 'apartment', - 'apartments', - 'casa', - 'maison', - 'home', - 'trump', - 'прив', - 'privé', - 'privado', - 'reserved' - ]), - Emoji( - name: 'chains', - char: '\u{26D3}\u{FE0F}', - shortName: 'chains', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.tool, - keywords: [ - 'chain', - 'uc5', - 'tool', - 'halloween', - 'steel', - 'shinobi', - 'tools', - 'samhain', - 'metal', - 'samurai' - ]), - Emoji( - name: 'hook', - char: '\u{1FA9D}', - shortName: 'hook', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.tool, - keywords: ['uc13', 'tool', 'steel', 'tools', 'metal']), - Emoji( - name: 'knot', - char: '\u{1FAA2}', - shortName: 'knot', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.artsCrafts, - keywords: [ - 'uc13', - 'boat', - 'rock climbing', - 'build', - 'rope', - 'tie', - 'boats', - 'boating', - 'climber', - 'cordage', - 'hitches', - 'bends', - 'splices', - 'loop' - ]), - Emoji( - name: 'magnet', - char: '\u{1F9F2}', - shortName: 'magnet', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.tool, - keywords: ['uc11', 'science', 'magnet', 'household', 'lab']), - Emoji( - name: 'pistol', - char: '\u{1F52B}', - shortName: 'gun', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.tool, - keywords: [ - 'gun', - 'handgun', - 'revolver', - 'tool', - 'weapon', - 'uc6', - 'weapon', - 'angry', - 'dead', - 'gun', - 'sarcastic', - 'deadpool', - 'danger', - 'soldier', - 'texas', - 'summer', - 'killer', - 'hunt', - 'shot', - 'war', - 'independence day', - 'weapons', - 'upset', - 'pissed', - 'pissed off', - 'unhappy', - 'frustrated', - 'anger', - 'rage', - 'frustration', - 'furious', - 'mad', - 'death', - 'die', - 'dying', - 'fart', - 'goth', - 'grave', - 'headstone', - 'horror', - 'hurt', - 'kill', - 'murder', - 'tomb', - 'toot', - 'died', - 'guns', - 'trigger', - 'sarcasm', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'weekend', - 'savage', - 'scary clown', - '4th of july' - ]), - Emoji( - name: 'bomb', - char: '\u{1F4A3}', - shortName: 'bomb', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.emotion, - keywords: [ - 'comic', - 'uc6', - 'weapon', - 'dead', - 'blast', - 'explosion', - 'deadpool', - 'power', - 'danger', - 'minecraft', - 'throw', - 'killer', - 'war', - 'weapons', - 'death', - 'die', - 'dying', - 'fart', - 'goth', - 'grave', - 'headstone', - 'horror', - 'hurt', - 'kill', - 'murder', - 'tomb', - 'toot', - 'died', - 'boom', - 'explode', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'savage', - 'scary clown' - ]), - Emoji( - name: 'firecracker', - char: '\u{1F9E8}', - shortName: 'firecracker', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.event, - keywords: [ - 'uc11', - 'weapon', - 'blast', - 'danger', - 'chinese', - 'throw', - 'war', - 'independence day', - 'shinobi', - 'weapons', - 'boom', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'chinois', - 'asian', - 'chine', - '4th of july', - 'samurai' - ]), - Emoji( - name: 'axe', - char: '\u{1FA93}', - shortName: 'axe', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.tool, - keywords: [ - 'uc12', - 'tool', - 'weapon', - 'halloween', - 'danger', - 'killer', - 'steel', - 'chopper', - 'knives', - 'chop', - 'tools', - 'weapons', - 'samhain', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'savage', - 'scary clown', - 'metal', - 'hatchet', - 'adz', - 'tomahawk' - ]), - Emoji( - name: 'carpentry saw', - char: '\u{1FA9A}', - shortName: 'carpentry_saw', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.tool, - keywords: [ - 'uc13', - 'tool', - 'weapon', - 'steel', - 'chop', - 'tools', - 'weapons', - 'metal' - ]), - Emoji( - name: 'kitchen knife', - char: '\u{1F52A}', - shortName: 'knife', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.dishware, - keywords: [ - 'cooking', - 'hocho', - 'knife', - 'tool', - 'weapon', - 'uc6', - 'tool', - 'weapon', - 'blood', - 'danger', - 'cutlery', - 'minecraft', - 'crazy', - 'killer', - 'hunt', - 'steel', - 'utensils', - 'knives', - 'chop', - 'tools', - 'weapons', - 'sangre', - 'sang', - 'blut', - 'sangue', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'dish', - 'weird', - 'awkward', - 'insane', - 'wild', - 'savage', - 'scary clown', - 'metal' - ]), - Emoji( - name: 'dagger', - char: '\u{1F5E1}\u{FE0F}', - shortName: 'dagger', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.tool, - keywords: [ - 'knife', - 'weapon', - 'uc7', - 'weapon', - 'halloween', - 'blood', - 'viking', - 'killer', - 'hunt', - 'steel', - 'knives', - 'weapons', - 'samhain', - 'sangre', - 'sang', - 'blut', - 'sangue', - 'knight', - 'savage', - 'scary clown', - 'metal' - ]), - Emoji( - name: 'crossed swords', - char: '\u{2694}\u{FE0F}', - shortName: 'crossed_swords', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.tool, - keywords: [ - 'crossed', - 'swords', - 'weapon', - 'uc4', - 'weapon', - 'japan', - 'dead', - 'deadpool', - 'danger', - 'viking', - 'minecraft', - 'killer', - 'steel', - 'war', - 'knives', - 'shinobi', - 'weapons', - 'japanese', - 'ninja', - 'death', - 'die', - 'dying', - 'fart', - 'goth', - 'grave', - 'headstone', - 'horror', - 'hurt', - 'kill', - 'murder', - 'tomb', - 'toot', - 'died', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'knight', - 'savage', - 'scary clown', - 'metal', - 'samurai' - ]), - Emoji( - name: 'shield', - char: '\u{1F6E1}\u{FE0F}', - shortName: 'shield', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.tool, - keywords: [ - 'weapon', - 'uc7', - 'harry potter', - 'viking', - 'minecraft', - 'knight' - ]), - Emoji( - name: 'cigarette', - char: '\u{1F6AC}', - shortName: 'smoking', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.otherObject, - keywords: [ - 'smoking', - 'uc6', - 'drugs', - 'smoking', - 'danger', - 'poison', - 'killer', - 'drug', - 'narcotics', - 'smoke', - 'cigarette', - 'puff', - 'warn', - 'attention', - 'caution', - 'alert', - 'error', - 'panic', - 'restricted', - "don't", - 'dont', - 'dangerous', - 'toxic', - 'toxins', - 'savage', - 'scary clown' - ]), - Emoji( - name: 'coffin', - char: '\u{26B0}\u{FE0F}', - shortName: 'coffin', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.otherObject, - keywords: [ - 'death', - 'uc4', - 'halloween', - 'dead', - 'rip', - 'condolence', - 'killer', - 'war', - 'covid', - 'samhain', - 'death', - 'die', - 'dying', - 'fart', - 'goth', - 'grave', - 'headstone', - 'horror', - 'hurt', - 'kill', - 'murder', - 'tomb', - 'toot', - 'died', - 'rest in peace', - 'compassion', - 'savage', - 'scary clown' - ]), - Emoji( - name: 'headstone', - char: '\u{1FAA6}', - shortName: 'headstone', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.otherObject, - keywords: [ - 'uc13', - 'halloween', - 'dead', - 'killer', - 'war', - 'memorial', - 'covid', - 'tombstone', - 'samhain', - 'death', - 'die', - 'dying', - 'fart', - 'goth', - 'grave', - 'headstone', - 'horror', - 'hurt', - 'kill', - 'murder', - 'tomb', - 'toot', - 'died', - 'savage', - 'scary clown' - ]), - Emoji( - name: 'funeral urn', - char: '\u{26B1}\u{FE0F}', - shortName: 'urn', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.otherObject, - keywords: [ - 'ashes', - 'death', - 'funeral', - 'urn', - 'uc4', - 'halloween', - 'dead', - 'rip', - 'condolence', - 'covid', - 'samhain', - 'death', - 'die', - 'dying', - 'fart', - 'goth', - 'grave', - 'headstone', - 'horror', - 'hurt', - 'kill', - 'murder', - 'tomb', - 'toot', - 'died', - 'rest in peace', - 'compassion' - ]), - Emoji( - name: 'amphora', - char: '\u{1F3FA}', - shortName: 'amphora', - emojiGroup: EmojiGroup.foodDrink, - emojiSubgroup: EmojiSubgroup.dishware, - keywords: [ - 'Aquarius', - 'cooking', - 'drink', - 'jug', - 'tool', - 'weapon', - 'zodiac', - 'uc8', - 'bling', - 'jewels', - 'gems', - 'jewel', - 'jewelry', - 'treasure' - ]), - Emoji( - name: 'magic wand', - char: '\u{1FA84}', - shortName: 'magic_wand', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.game, - keywords: [ - 'uc13', - 'harry potter', - 'disney', - 'wizard', - 'cartoon', - 'Sorcerer', - 'Sorceress', - 'witch' - ]), - Emoji( - name: 'crystal ball', - char: '\u{1F52E}', - shortName: 'crystal_ball', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.game, - keywords: [ - 'ball', - 'crystal', - 'fairy tale', - 'fantasy', - 'fortune', - 'tool', - 'uc6', - 'halloween', - 'ball', - 'harry potter', - 'magic', - 'disney', - 'bling', - 'mirror', - 'future', - 'mystery', - 'wizard', - 'fantasy', - 'energy', - 'snow white', - 'samhain', - 'balls', - 'ballon', - 'spell', - 'genie', - 'magical', - 'cartoon', - 'jewels', - 'gems', - 'jewel', - 'jewelry', - 'treasure', - 'Sorcerer', - 'Sorceress', - 'witch' - ]), - Emoji( - name: 'prayer beads', - char: '\u{1F4FF}', - shortName: 'prayer_beads', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'beads', - 'clothing', - 'necklace', - 'prayer', - 'religion', - 'uc8', - 'religion', - 'rosary', - 'pray', - 'jesus', - 'bible', - 'prayer', - 'praying', - 'prayers', - 'grateful', - 'sorry', - 'heaven', - 'bless', - 'faith', - 'holy', - 'spirit', - 'hopeful', - 'blessed', - 'preach', - 'offering' - ]), - Emoji( - name: 'nazar amulet', - char: '\u{1F9FF}', - shortName: 'nazar_amulet', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.game, - keywords: [ - 'uc11', - 'game', - 'eyes', - 'luck', - 'magic', - 'evil', - 'fantasy', - 'eye bead', - 'games', - 'gaming', - 'eye', - 'eyebrow', - 'good luck', - 'lucky', - 'spell', - 'genie', - 'magical', - 'imp', - 'demon', - 'devil', - 'naughty', - 'devilish', - 'diablo', - 'diable', - 'satan', - 'Nazar Boncuğu', - 'Munçuk', - 'turkish' - ]), - Emoji( - name: 'barber pole', - char: '\u{1F488}', - shortName: 'barber', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.placeOther, - keywords: ['barber', 'haircut', 'pole', 'uc6']), - Emoji( - name: 'alembic', - char: '\u{2697}\u{FE0F}', - shortName: 'alembic', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.science, - keywords: [ - 'chemistry', - 'tool', - 'uc4', - 'classroom', - 'science', - 'poison', - 'minecraft', - 'measure', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'lab', - 'toxic', - 'toxins' - ]), - Emoji( - name: 'telescope', - char: '\u{1F52D}', - shortName: 'telescope', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.science, - keywords: [ - 'science', - 'tool', - 'uc6', - 'space', - 'star', - 'science', - 'search', - 'outer space', - 'galaxy', - 'universe', - 'nasa', - 'spaceship', - 'stars', - 'lab', - 'look', - 'find', - 'looking', - 'see' - ]), - Emoji( - name: 'microscope', - char: '\u{1F52C}', - shortName: 'microscope', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.science, - keywords: [ - 'science', - 'tool', - 'uc6', - 'classroom', - 'science', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'lab' - ]), - Emoji( - name: 'hole', - char: '\u{1F573}\u{FE0F}', - shortName: 'hole', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.emotion, - keywords: ['hole', 'uc7', 'trap']), - Emoji( - name: 'window', - char: '\u{1FA9F}', - shortName: 'window', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.household, - keywords: [ - 'uc13', - 'house', - 'sky', - 'day', - 'household', - 'daydream', - 'houses', - 'apartment', - 'apartments', - 'casa', - 'maison', - 'home' - ]), - Emoji( - name: 'adhesive bandage', - char: '\u{1FA79}', - shortName: 'adhesive_bandage', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.medical, - keywords: [ - 'uc12', - 'health', - '911', - 'nurse', - 'bandaid', - 'medical', - 'medicine', - 'doctor', - 'emergency', - 'injury' - ]), - Emoji( - name: 'stethoscope', - char: '\u{1FA7A}', - shortName: 'stethoscope', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.medical, - keywords: [ - 'uc12', - 'health', - '911', - 'nurse', - 'heart', - 'auscultation', - 'covid', - 'medical', - 'medicine', - 'doctor', - 'emergency', - 'injury', - 'hearts', - 'serce', - 'corazón', - 'coração', - 'coeur' - ]), - Emoji( - name: 'pill', - char: '\u{1F48A}', - shortName: 'pill', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.medical, - keywords: [ - 'doctor', - 'medicine', - 'sick', - 'uc6', - 'drugs', - 'health', - 'nurse', - 'poison', - 'killer', - 'medical', - 'drug', - 'narcotics', - 'medicine', - 'doctor', - 'toxic', - 'toxins', - 'savage', - 'scary clown' - ]), - Emoji( - name: 'syringe', - char: '\u{1F489}', - shortName: 'syringe', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.medical, - keywords: [ - 'doctor', - 'medicine', - 'needle', - 'shot', - 'sick', - 'tool', - 'uc6', - 'weapon', - 'drugs', - 'dead', - 'health', - '911', - 'blood', - 'nurse', - 'poison', - 'killer', - 'shot', - 'needle', - 'bleed', - 'covid', - 'medical', - 'weapons', - 'drug', - 'narcotics', - 'death', - 'die', - 'dying', - 'fart', - 'goth', - 'grave', - 'headstone', - 'horror', - 'hurt', - 'kill', - 'murder', - 'tomb', - 'toot', - 'died', - 'medicine', - 'doctor', - 'emergency', - 'injury', - 'sangre', - 'sang', - 'blut', - 'sangue', - 'toxic', - 'toxins', - 'savage', - 'scary clown', - 'donation', - 'menstruation' - ]), - Emoji( - name: 'drop of blood', - char: '\u{1FA78}', - shortName: 'drop_of_blood', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.medical, - keywords: [ - 'uc12', - 'body', - 'science', - 'health', - '911', - 'blood', - 'vampire', - 'shot', - 'bleed', - 'medical', - 'body part', - 'anatomy', - 'lab', - 'medicine', - 'doctor', - 'emergency', - 'injury', - 'sangre', - 'sang', - 'blut', - 'sangue', - 'dracula', - 'donation', - 'menstruation' - ]), - Emoji( - name: 'dna', - char: '\u{1F9EC}', - shortName: 'dna', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.science, - keywords: [ - 'uc11', - 'family', - 'body', - 'science', - 'blood', - 'history', - 'future', - 'deoxyribonucleic acid', - 'medical', - 'families', - 'group', - 'brother', - 'sister', - 'daughter', - 'son', - 'together', - 'sibling', - 'twins', - 'brothers', - 'sisters', - 'body part', - 'anatomy', - 'lab', - 'sangre', - 'sang', - 'blut', - 'sangue', - 'ancient', - 'old', - 'gene', - 'genetic code', - 'RNA', - 'chromosome', - 'heredity', - 'nucleic acid' - ]), - Emoji( - name: 'microbe', - char: '\u{1F9A0}', - shortName: 'microbe', - emojiGroup: EmojiGroup.animalsNature, - emojiSubgroup: EmojiSubgroup.animalBug, - keywords: [ - 'uc11', - 'body', - 'science', - 'stinky', - 'bacteria', - 'booger', - 'virus', - 'covid', - 'medical', - 'body part', - 'anatomy', - 'lab', - 'smell', - 'stink', - 'odor', - 'microorganism', - 'bacterium', - 'corona' - ]), - Emoji( - name: 'petri dish', - char: '\u{1F9EB}', - shortName: 'petri_dish', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.science, - keywords: [ - 'uc11', - 'classroom', - 'science', - 'bacteria', - 'petrie dish', - 'mushroom', - 'virus', - 'covid', - 'medical', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'lab', - 'microorganism', - 'bacterium', - 'petri plate', - 'cell culture dish', - 'moss', - 'corona' - ]), - Emoji( - name: 'test tube', - char: '\u{1F9EA}', - shortName: 'test_tube', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.science, - keywords: [ - 'uc11', - 'science', - 'poison', - 'test-tube', - 'measure', - 'medical', - 'lab', - 'toxic', - 'toxins', - 'culture tube', - 'sample tube' - ]), - Emoji( - name: 'thermometer', - char: '\u{1F321}\u{FE0F}', - shortName: 'thermometer', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.skyWeather, - keywords: [ - 'weather', - 'uc7', - 'science', - 'health', - 'hot', - 'virus', - 'measure', - 'medical', - 'lab', - 'medicine', - 'doctor', - 'heat', - 'warm', - 'caliente', - 'chaud', - 'heiß', - 'corona' - ]), - Emoji( - name: 'mouse trap', - char: '\u{1FAA4}', - shortName: 'mouse_trap', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.household, - keywords: ['uc13', 'trap', 'household', 'rodent']), - Emoji( - name: 'broom', - char: '\u{1F9F9}', - shortName: 'broom', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.household, - keywords: ['uc11', 'clean', 'sweep', 'household', 'dust', 'mop']), - Emoji( - name: 'basket', - char: '\u{1F9FA}', - shortName: 'basket', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.household, - keywords: [ - 'uc11', - 'household', - 'sew', - 'knit', - 'embroider', - 'stitch', - 'repair', - 'crochet', - 'alter', - 'seamstress', - 'fix' - ]), - Emoji( - name: 'sewing needle', - char: '\u{1FAA1}', - shortName: 'sewing_needle', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.artsCrafts, - keywords: [ - 'uc13', - 'bathroom', - 'needle', - 'household', - 'sew', - 'knit', - 'embroider', - 'stitch', - 'repair', - 'crochet', - 'alter', - 'seamstress', - 'fix' - ]), - Emoji( - name: 'roll of paper', - char: '\u{1F9FB}', - shortName: 'roll_of_paper', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.household, - keywords: [ - 'uc11', - 'bathroom', - 'diarrhea', - 'shit', - 'clean', - 'household', - 'shits', - 'the shits', - 'poop', - 'turd', - 'feces', - 'pile', - 'merde', - 'butthole', - 'caca', - 'crap', - 'dirty', - 'pooo', - 'mess', - 'brown', - 'poopoo' - ]), - Emoji( - name: 'toilet', - char: '\u{1F6BD}', - shortName: 'toilet', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.household, - keywords: [ - 'toilet', - 'uc6', - 'bathroom', - 'sick', - 'diarrhea', - 'shit', - 'private', - 'household', - 'throne', - 'barf', - 'vomit', - 'throw up', - 'puke', - 'get well', - 'cough', - 'puking', - 'barfing', - 'malade', - 'spew', - 'shits', - 'the shits', - 'poop', - 'turd', - 'feces', - 'pile', - 'merde', - 'butthole', - 'caca', - 'crap', - 'dirty', - 'pooo', - 'mess', - 'brown', - 'poopoo', - 'прив', - 'privé', - 'privado', - 'reserved' - ]), - Emoji( - name: 'plunger', - char: '\u{1FAA0}', - shortName: 'plunger', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.household, - keywords: [ - 'uc13', - 'bathroom', - 'shit', - 'household', - 'poop', - 'turd', - 'feces', - 'pile', - 'merde', - 'butthole', - 'caca', - 'crap', - 'dirty', - 'pooo', - 'mess', - 'brown', - 'poopoo' - ]), - Emoji( - name: 'bucket', - char: '\u{1FAA3}', - shortName: 'bucket', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.household, - keywords: ['uc13', 'household', 'pail', 'vessel']), - Emoji( - name: 'potable water', - char: '\u{1F6B0}', - shortName: 'potable_water', - emojiGroup: EmojiGroup.symbols, - emojiSubgroup: EmojiSubgroup.transportSign, - keywords: [ - 'drinking', - 'potable', - 'water', - 'uc6', - 'drip', - 'water', - 'household', - 'water drop' - ]), - Emoji( - name: 'shower', - char: '\u{1F6BF}', - shortName: 'shower', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.household, - keywords: [ - 'water', - 'uc6', - 'bathroom', - 'clean', - 'wash', - 'shower', - 'bathe', - 'bathing', - 'washing' - ]), - Emoji( - name: 'bathtub', - char: '\u{1F6C1}', - shortName: 'bathtub', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.household, - keywords: [ - 'bath', - 'uc6', - 'bathroom', - 'steam', - 'clean', - 'wash', - 'steaming', - 'piping', - 'shower', - 'bathe', - 'bathing', - 'washing' - ]), - Emoji( - name: 'person taking bath', - char: '\u{1F6C0}', - shortName: 'bath', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'bath', - 'bathtub', - 'uc6', - 'diversity', - 'bathroom', - 'steam', - 'clean', - 'wash', - 'spa', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'steaming', - 'piping', - 'shower', - 'bathe', - 'bathing', - 'washing', - 'relax', - 'sauna' - ]), - Emoji( - name: 'person taking bath: light skin tone', - char: '\u{1F6C0}\u{1F3FB}', - shortName: 'bath_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'bath', - 'bathtub', - 'light skin tone', - 'uc8', - 'diversity', - 'bathroom', - 'steam', - 'clean', - 'wash', - 'spa', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'steaming', - 'piping', - 'shower', - 'bathe', - 'bathing', - 'washing', - 'relax', - 'sauna' - ], - modifiable: true), - Emoji( - name: 'person taking bath: medium-light skin tone', - char: '\u{1F6C0}\u{1F3FC}', - shortName: 'bath_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'bath', - 'bathtub', - 'medium-light skin tone', - 'uc8', - 'diversity', - 'bathroom', - 'steam', - 'clean', - 'wash', - 'spa', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'steaming', - 'piping', - 'shower', - 'bathe', - 'bathing', - 'washing', - 'relax', - 'sauna' - ], - modifiable: true), - Emoji( - name: 'person taking bath: medium skin tone', - char: '\u{1F6C0}\u{1F3FD}', - shortName: 'bath_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'bath', - 'bathtub', - 'medium skin tone', - 'uc8', - 'diversity', - 'bathroom', - 'steam', - 'clean', - 'wash', - 'spa', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'steaming', - 'piping', - 'shower', - 'bathe', - 'bathing', - 'washing', - 'relax', - 'sauna' - ], - modifiable: true), - Emoji( - name: 'person taking bath: medium-dark skin tone', - char: '\u{1F6C0}\u{1F3FE}', - shortName: 'bath_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'bath', - 'bathtub', - 'medium-dark skin tone', - 'uc8', - 'diversity', - 'bathroom', - 'steam', - 'clean', - 'wash', - 'spa', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'steaming', - 'piping', - 'shower', - 'bathe', - 'bathing', - 'washing', - 'relax', - 'sauna' - ], - modifiable: true), - Emoji( - name: 'person taking bath: dark skin tone', - char: '\u{1F6C0}\u{1F3FF}', - shortName: 'bath_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'bath', - 'bathtub', - 'dark skin tone', - 'uc8', - 'diversity', - 'bathroom', - 'steam', - 'clean', - 'wash', - 'spa', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'steaming', - 'piping', - 'shower', - 'bathe', - 'bathing', - 'washing', - 'relax', - 'sauna' - ], - modifiable: true), - Emoji( - name: 'toothbrush', - char: '\u{1FAA5}', - shortName: 'toothbrush', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.household, - keywords: ['uc13', 'bathroom']), - Emoji( - name: 'soap', - char: '\u{1F9FC}', - shortName: 'soap', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.household, - keywords: [ - 'uc11', - 'bathroom', - 'health', - 'pink', - 'clean', - 'wash', - 'household', - 'dishes', - 'savon', - 'covid', - 'medicine', - 'doctor', - 'rose', - 'shower', - 'bathe', - 'bathing', - 'washing' - ]), - Emoji( - name: 'razor', - char: '\u{1FA92}', - shortName: 'razor', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.household, - keywords: [ - 'uc12', - 'weapon', - 'mustache', - 'beard', - 'blade', - 'weapons', - 'shave', - 'trim' - ]), - Emoji( - name: 'sponge', - char: '\u{1F9FD}', - shortName: 'sponge', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.household, - keywords: [ - 'uc11', - 'bathroom', - 'clean', - 'wash', - 'household', - 'dishes', - 'shower', - 'bathe', - 'bathing', - 'washing' - ]), - Emoji( - name: 'lotion bottle', - char: '\u{1F9F4}', - shortName: 'squeeze_bottle', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.household, - keywords: [ - 'uc11', - 'bathroom', - 'beach', - 'clean', - 'wash', - 'picnic', - 'household', - 'dishes', - 'savon', - 'covid', - 'shower', - 'bathe', - 'bathing', - 'washing' - ]), - Emoji( - name: 'bellhop bell', - char: '\u{1F6CE}\u{FE0F}', - shortName: 'bellhop', - emojiGroup: EmojiGroup.travelPlaces, - emojiSubgroup: EmojiSubgroup.hotel, - keywords: [ - 'bell', - 'bellhop', - 'hotel', - 'uc7', - 'vacation', - 'help', - 'suitcase', - 'hotel', - 'carry-on', - 'vacancy', - 'no vacancy' - ]), - Emoji( - name: 'key', - char: '\u{1F511}', - shortName: 'key', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.lock, - keywords: [ - 'lock', - 'password', - 'uc6', - 'lock', - 'household', - 'locks', - 'key', - 'keys' - ]), - Emoji( - name: 'old key', - char: '\u{1F5DD}\u{FE0F}', - shortName: 'key2', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.lock, - keywords: [ - 'clue', - 'key', - 'lock', - 'old', - 'uc7', - 'lock', - 'harry potter', - 'household', - 'locks', - 'key', - 'keys' - ]), - Emoji( - name: 'door', - char: '\u{1F6AA}', - shortName: 'door', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.household, - keywords: [ - 'door', - 'uc6', - 'minecraft', - 'hotel', - 'household', - 'vacancy', - 'no vacancy' - ]), - Emoji( - name: 'chair', - char: '\u{1FA91}', - shortName: 'chair', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.household, - keywords: [ - 'uc12', - 'household', - 'sit', - 'seat', - 'throne', - 'sitting', - 'kneel', - 'kneeling', - 'bench', - 'sedia', - 'Stuhl', - 'chaise', - 'silla', - 'armchair' - ]), - Emoji( - name: 'mirror', - char: '\u{1FA9E}', - shortName: 'mirror', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.household, - keywords: [ - 'uc13', - 'bathroom', - 'beautiful', - 'disney', - 'mirror', - 'household', - 'snow white', - 'cute', - 'pretty', - 'adorable', - 'adore', - 'beauty', - 'cutie', - 'babe', - 'lovely', - 'cartoon' - ]), - Emoji( - name: 'couch and lamp', - char: '\u{1F6CB}\u{FE0F}', - shortName: 'couch', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.household, - keywords: [ - 'couch', - 'hotel', - 'lamp', - 'uc7', - 'tired', - 'light', - 'sofa', - 'hotel', - 'household', - 'seat', - 'sleepy', - 'sleep', - 'dormi', - 'pillow', - 'blanket', - 'exhausted', - 'lamp', - 'light bulb', - 'flashlight', - 'spotlight', - 'illuminate', - 'lightbulb', - 'lighting', - 'luce', - 'licht', - 'lumière', - 'luz', - 'vacancy', - 'no vacancy', - 'bench', - 'sedia', - 'Stuhl', - 'chaise', - 'silla', - 'armchair' - ]), - Emoji( - name: 'bed', - char: '\u{1F6CF}\u{FE0F}', - shortName: 'bed', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.household, - keywords: [ - 'hotel', - 'sleep', - 'uc7', - 'tired', - 'hotel', - 'household', - 'sleepy', - 'sleep', - 'dormi', - 'pillow', - 'blanket', - 'exhausted', - 'vacancy', - 'no vacancy' - ]), - Emoji( - name: 'person in bed', - char: '\u{1F6CC}', - shortName: 'sleeping_accommodation', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'hotel', - 'sleep', - 'uc7', - 'diversity', - 'tired', - 'lazy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'sleepy', - 'sleep', - 'dormi', - 'pillow', - 'blanket', - 'exhausted' - ]), - Emoji( - name: 'person in bed: light skin tone', - char: '\u{1F6CC}\u{1F3FB}', - shortName: 'person_in_bed_tone1', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'hotel', - 'light skin tone', - 'sleep', - 'uc8', - 'diversity', - 'tired', - 'lazy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'sleepy', - 'sleep', - 'dormi', - 'pillow', - 'blanket', - 'exhausted' - ], - modifiable: true), - Emoji( - name: 'person in bed: medium-light skin tone', - char: '\u{1F6CC}\u{1F3FC}', - shortName: 'person_in_bed_tone2', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'hotel', - 'medium-light skin tone', - 'sleep', - 'uc8', - 'diversity', - 'tired', - 'lazy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'sleepy', - 'sleep', - 'dormi', - 'pillow', - 'blanket', - 'exhausted' - ], - modifiable: true), - Emoji( - name: 'person in bed: medium skin tone', - char: '\u{1F6CC}\u{1F3FD}', - shortName: 'person_in_bed_tone3', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'hotel', - 'medium skin tone', - 'sleep', - 'uc8', - 'diversity', - 'tired', - 'lazy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'sleepy', - 'sleep', - 'dormi', - 'pillow', - 'blanket', - 'exhausted' - ], - modifiable: true), - Emoji( - name: 'person in bed: medium-dark skin tone', - char: '\u{1F6CC}\u{1F3FE}', - shortName: 'person_in_bed_tone4', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'hotel', - 'medium-dark skin tone', - 'sleep', - 'uc8', - 'diversity', - 'tired', - 'lazy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'sleepy', - 'sleep', - 'dormi', - 'pillow', - 'blanket', - 'exhausted' - ], - modifiable: true), - Emoji( - name: 'person in bed: dark skin tone', - char: '\u{1F6CC}\u{1F3FF}', - shortName: 'person_in_bed_tone5', - emojiGroup: EmojiGroup.peopleBody, - emojiSubgroup: EmojiSubgroup.personResting, - keywords: [ - 'dark skin tone', - 'hotel', - 'sleep', - 'uc8', - 'diversity', - 'tired', - 'lazy', - 'diverse', - 'modifier', - 'modifiers', - 'equality', - 'sleepy', - 'sleep', - 'dormi', - 'pillow', - 'blanket', - 'exhausted' - ], - modifiable: true), - Emoji( - name: 'teddy bear', - char: '\u{1F9F8}', - shortName: 'teddy_bear', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.game, - keywords: [ - 'uc11', - 'animal', - 'baby', - 'play', - 'gummy', - 'household', - 'stuffed animal', - 'toy', - 'animals', - 'animal kingdom', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'doudou' - ]), - Emoji( - name: 'framed picture', - char: '\u{1F5BC}\u{FE0F}', - shortName: 'frame_photo', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.artsCrafts, - keywords: [ - 'art', - 'frame', - 'museum', - 'painting', - 'picture', - 'uc7', - 'theatre', - 'travel', - 'vacation', - 'painting', - 'image', - 'instagram', - 'household', - 'theater', - 'craft', - 'drama', - 'monet', - 'painter', - 'arts' - ]), - Emoji( - name: 'shopping bags', - char: '\u{1F6CD}\u{FE0F}', - shortName: 'shopping_bags', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.clothing, - keywords: [ - 'bag', - 'hotel', - 'shopping', - 'uc7', - 'bag', - 'gift', - 'birthday', - 'happy birthday', - 'celebrate', - 'rich', - 'purchase', - 'swag', - 'present', - 'cadeau', - 'bows', - 'presents', - 'birth', - 'cumpleaños', - 'anniversaire', - 'bday', - 'Bon anniversaire', - 'joyeux anniversaire', - 'buon compleanno', - 'feliz cumpleaños', - 'alles Gute zum Geburtstag', - 'feliz Aniversário', - 'Gratulerer med dagen', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'grand', - 'expensive', - 'fancy', - 'buy', - 'shop', - 'spend' - ]), - Emoji( - name: 'shopping cart', - char: '\u{1F6D2}', - shortName: 'shopping_cart', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.household, - keywords: [ - 'cart', - 'shopping', - 'trolley', - 'uc9', - 'food', - 'purchase', - 'foods', - 'eat', - 'meal', - 'comida', - 'nourriture', - 'eats', - 'groceries', - 'grocery', - 'hungry', - 'tasty', - 'mmm', - 'yummy', - 'feed', - 'hunger', - 'buy', - 'shop', - 'spend' - ]), - Emoji( - name: 'wrapped gift', - char: '\u{1F381}', - shortName: 'gift', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.event, - keywords: [ - 'box', - 'celebration', - 'gift', - 'present', - 'wrapped', - 'uc6', - 'holidays', - 'love', - 'gift', - 'birthday', - 'christmas', - 'happy birthday', - 'celebrate', - 'holiday', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'present', - 'cadeau', - 'bows', - 'presents', - 'birth', - 'cumpleaños', - 'anniversaire', - 'bday', - 'navidad', - 'xmas', - 'noel', - 'merry christmas', - 'Bon anniversaire', - 'joyeux anniversaire', - 'buon compleanno', - 'feliz cumpleaños', - 'alles Gute zum Geburtstag', - 'feliz Aniversário', - 'Gratulerer med dagen', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar' - ]), - Emoji( - name: 'balloon', - char: '\u{1F388}', - shortName: 'balloon', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.event, - keywords: [ - 'celebration', - 'uc6', - 'holidays', - 'baby', - 'birthday', - 'good', - 'balloons', - 'happy birthday', - 'celebrate', - 'independence day', - 'sperm', - 'toy', - 'holiday', - 'kid', - 'babies', - 'infant', - 'infants', - 'crying kid', - 'bebe', - 'little', - 'petite', - 'bambino', - 'birth', - 'cumpleaños', - 'anniversaire', - 'bday', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'Bon anniversaire', - 'joyeux anniversaire', - 'buon compleanno', - 'feliz cumpleaños', - 'alles Gute zum Geburtstag', - 'feliz Aniversário', - 'Gratulerer med dagen', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - '4th of july' - ]), - Emoji( - name: 'carp streamer', - char: '\u{1F38F}', - shortName: 'flags', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.event, - keywords: [ - 'carp', - 'celebration', - 'streamer', - 'uc6', - 'japan', - 'japanese', - 'ninja' - ]), - Emoji( - name: 'ribbon', - char: '\u{1F380}', - shortName: 'ribbon', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.event, - keywords: [ - 'celebration', - 'uc6', - 'holidays', - 'love', - 'gift', - 'birthday', - 'accessories', - 'happy birthday', - 'celebrate', - 'rich', - 'holiday', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'present', - 'cadeau', - 'bows', - 'presents', - 'birth', - 'cumpleaños', - 'anniversaire', - 'bday', - 'Bon anniversaire', - 'joyeux anniversaire', - 'buon compleanno', - 'feliz cumpleaños', - 'alles Gute zum Geburtstag', - 'feliz Aniversário', - 'Gratulerer med dagen', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'grand', - 'expensive', - 'fancy' - ]), - Emoji( - name: 'confetti ball', - char: '\u{1F38A}', - shortName: 'confetti_ball', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.event, - keywords: [ - 'ball', - 'celebration', - 'confetti', - 'uc6', - 'happy', - 'birthday', - 'cheers', - 'girls night', - 'boys night', - 'happy birthday', - 'confetti', - 'celebrate', - 'glitter', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'birth', - 'cumpleaños', - 'anniversaire', - 'bday', - 'gān bēi', - 'Na zdravi', - 'Proost', - 'Prost', - 'Sláinte', - 'Cin cin', - 'Kanpai', - 'Na zdrowie', - 'Saúde', - 'На здоровье', - 'Salud', - 'Skål', - 'Sei gesund', - 'santé', - 'ladies night', - 'girls only', - 'girlfriend', - 'guys night', - 'Bon anniversaire', - 'joyeux anniversaire', - 'buon compleanno', - 'feliz cumpleaños', - 'alles Gute zum Geburtstag', - 'feliz Aniversário', - 'Gratulerer med dagen', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar' - ]), - Emoji( - name: 'party popper', - char: '\u{1F389}', - shortName: 'tada', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.event, - keywords: [ - 'celebration', - 'party', - 'popper', - 'tada', - 'uc6', - 'holidays', - 'happy', - 'birthday', - 'cheers', - 'good', - 'girls night', - 'boys night', - 'happy birthday', - 'confetti', - 'celebrate', - 'glitter', - 'bingo', - 'fame', - 'fun', - 'independence day', - 'holiday', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'birth', - 'cumpleaños', - 'anniversaire', - 'bday', - 'gān bēi', - 'Na zdravi', - 'Proost', - 'Prost', - 'Sláinte', - 'Cin cin', - 'Kanpai', - 'Na zdrowie', - 'Saúde', - 'На здоровье', - 'Salud', - 'Skål', - 'Sei gesund', - 'santé', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'ladies night', - 'girls only', - 'girlfriend', - 'guys night', - 'Bon anniversaire', - 'joyeux anniversaire', - 'buon compleanno', - 'feliz cumpleaños', - 'alles Gute zum Geburtstag', - 'feliz Aniversário', - 'Gratulerer med dagen', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'famous', - 'celebrity', - '4th of july' - ]), - Emoji( - name: 'piñata', - char: '\u{1FA85}', - shortName: 'piñata', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.game, - keywords: [ - 'uc13', - 'mexican', - 'birthday', - 'happy birthday', - 'celebrate', - 'pinata', - 'mexico', - 'cinco de mayo', - 'español', - 'birth', - 'cumpleaños', - 'anniversaire', - 'bday', - 'Bon anniversaire', - 'joyeux anniversaire', - 'buon compleanno', - 'feliz cumpleaños', - 'alles Gute zum Geburtstag', - 'feliz Aniversário', - 'Gratulerer med dagen', - 'celebration', - 'event', - 'celebrating', - 'festa', - 'parties', - 'events', - 'new years', - 'new year', - 'fiesta', - 'fete', - 'newyear', - 'party', - 'festive', - 'festival', - 'yolo', - 'festejar', - 'papier-mâché', - 'paper mache', - 'pignatta', - 'dahi handi', - 'fer cagar el tió', - 'suikawari', - 'pukpok-palayok', - 'cartonería' - ]), - Emoji( - name: 'nesting dolls', - char: '\u{1FA86}', - shortName: 'nesting_dolls', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.game, - keywords: [ - 'uc13', - 'russian', - 'toy', - 'matryoshka dolls', - 'babushka dolls', - 'stacking dolls', - 'russian dolls' - ]), - Emoji( - name: 'Japanese dolls', - char: '\u{1F38E}', - shortName: 'dolls', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.event, - keywords: [ - 'Japanese', - 'celebration', - 'doll', - 'festival', - 'uc6', - 'japan', - 'japanese', - 'ninja' - ]), - Emoji( - name: 'red paper lantern', - char: '\u{1F3EE}', - shortName: 'izakaya_lantern', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.lightVideo, - keywords: [ - 'bar', - 'lantern', - 'light', - 'red', - 'uc6', - 'japan', - 'japanese', - 'ninja' - ]), - Emoji( - name: 'wind chime', - char: '\u{1F390}', - shortName: 'wind_chime', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.event, - keywords: [ - 'bell', - 'celebration', - 'chime', - 'wind', - 'uc6', - 'japan', - 'japanese', - 'ninja' - ]), - Emoji( - name: 'red envelope', - char: '\u{1F9E7}', - shortName: 'red_envelope', - emojiGroup: EmojiGroup.activities, - emojiSubgroup: EmojiSubgroup.event, - keywords: [ - 'uc11', - 'gift', - 'chinese', - 'present', - 'cadeau', - 'bows', - 'presents', - 'chinois', - 'asian', - 'chine' - ]), - Emoji( - name: 'envelope', - char: '\u{2709}\u{FE0F}', - shortName: 'envelope', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.mail, - keywords: [ - 'email', - 'letter', - 'uc1', - 'write', - 'mail', - 'envelope', - 'work', - 'writing', - 'email', - 'post', - 'post office', - 'letter', - 'message', - 'offer', - 'office' - ]), - Emoji( - name: 'envelope with arrow', - char: '\u{1F4E9}', - shortName: 'envelope_with_arrow', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.mail, - keywords: [ - 'arrow', - 'down', - 'e-mail', - 'email', - 'envelope', - 'letter', - 'mail', - 'outgoing', - 'sent', - 'uc6', - 'mail', - 'envelope', - 'download', - 'work', - 'email', - 'post', - 'post office', - 'letter', - 'message', - 'offer', - 'office' - ]), - Emoji( - name: 'incoming envelope', - char: '\u{1F4E8}', - shortName: 'incoming_envelope', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.mail, - keywords: [ - 'e-mail', - 'email', - 'envelope', - 'incoming', - 'letter', - 'mail', - 'receive', - 'uc6', - 'mail', - 'envelope', - 'work', - 'email', - 'post', - 'post office', - 'letter', - 'message', - 'offer', - 'office' - ]), - Emoji( - name: 'e-mail', - char: '\u{1F4E7}', - shortName: 'e-mail', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.mail, - keywords: [ - 'email', - 'letter', - 'mail', - 'uc6', - 'classroom', - 'mail', - 'business', - 'envelope', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'email', - 'post', - 'post office', - 'letter', - 'message', - 'offer', - 'office' - ]), - Emoji( - name: 'love letter', - char: '\u{1F48C}', - shortName: 'love_letter', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.emotion, - keywords: [ - 'heart', - 'letter', - 'love', - 'mail', - 'uc6', - 'love', - 'mail', - 'envelope', - 'pink', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'email', - 'post', - 'post office', - 'letter', - 'message', - 'offer', - 'rose' - ]), - Emoji( - name: 'inbox tray', - char: '\u{1F4E5}', - shortName: 'inbox_tray', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.mail, - keywords: [ - 'box', - 'inbox', - 'letter', - 'mail', - 'receive', - 'tray', - 'uc6', - 'business', - 'envelope', - 'work', - 'letter', - 'message', - 'offer', - 'office' - ]), - Emoji( - name: 'outbox tray', - char: '\u{1F4E4}', - shortName: 'outbox_tray', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.mail, - keywords: [ - 'box', - 'letter', - 'mail', - 'outbox', - 'sent', - 'tray', - 'uc6', - 'business', - 'work', - 'office' - ]), - Emoji( - name: 'package', - char: '\u{1F4E6}', - shortName: 'package', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.mail, - keywords: [ - 'box', - 'parcel', - 'uc6', - 'classroom', - 'gift', - 'mail', - 'moving', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'present', - 'cadeau', - 'bows', - 'presents', - 'email', - 'post', - 'post office', - 'office' - ]), - Emoji( - name: 'label', - char: '\u{1F3F7}\u{FE0F}', - shortName: 'label', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.bookPaper, - keywords: ['label', 'uc7', 'discount', 'price', 'sale', 'bargain']), - Emoji( - name: 'closed mailbox with lowered flag', - char: '\u{1F4EA}', - shortName: 'mailbox_closed', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.mail, - keywords: [ - 'closed', - 'lowered', - 'mail', - 'mailbox', - 'postbox', - 'uc6', - 'mail', - 'envelope', - 'household', - 'email', - 'post', - 'post office', - 'letter', - 'message', - 'offer' - ]), - Emoji( - name: 'closed mailbox with raised flag', - char: '\u{1F4EB}', - shortName: 'mailbox', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.mail, - keywords: [ - 'closed', - 'mail', - 'mailbox', - 'postbox', - 'uc6', - 'mail', - 'envelope', - 'email', - 'post', - 'post office', - 'letter', - 'message', - 'offer' - ]), - Emoji( - name: 'open mailbox with raised flag', - char: '\u{1F4EC}', - shortName: 'mailbox_with_mail', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.mail, - keywords: [ - 'mail', - 'mailbox', - 'open', - 'postbox', - 'uc6', - 'mail', - 'envelope', - 'household', - 'email', - 'post', - 'post office', - 'letter', - 'message', - 'offer' - ]), - Emoji( - name: 'open mailbox with lowered flag', - char: '\u{1F4ED}', - shortName: 'mailbox_with_no_mail', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.mail, - keywords: [ - 'lowered', - 'mail', - 'mailbox', - 'open', - 'postbox', - 'uc6', - 'mail', - 'envelope', - 'empty', - 'email', - 'post', - 'post office', - 'letter', - 'message', - 'offer' - ]), - Emoji( - name: 'postbox', - char: '\u{1F4EE}', - shortName: 'postbox', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.mail, - keywords: [ - 'mail', - 'mailbox', - 'uc6', - 'mail', - 'envelope', - 'email', - 'post', - 'post office', - 'letter', - 'message', - 'offer' - ]), - Emoji( - name: 'postal horn', - char: '\u{1F4EF}', - shortName: 'postal_horn', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.sound, - keywords: [ - 'horn', - 'post', - 'postal', - 'uc6', - 'instruments', - 'music', - 'instrument', - 'singing', - 'concert', - 'jaz', - 'listen', - 'singer', - 'song', - 'musique' - ]), - Emoji( - name: 'placard', - char: '\u{1FAA7}', - shortName: 'placard', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.otherObject, - keywords: [ - 'uc13', - 'peace', - 'protest', - 'peace out', - 'peace sign', - 'blm', - 'demonstration' - ]), - Emoji( - name: 'scroll', - char: '\u{1F4DC}', - shortName: 'scroll', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.bookPaper, - keywords: [ - 'paper', - 'uc6', - 'classroom', - 'harry potter', - 'document', - 'scroll', - 'history', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'documents', - 'ancient', - 'old', - 'office' - ]), - Emoji( - name: 'page with curl', - char: '\u{1F4C3}', - shortName: 'page_with_curl', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.bookPaper, - keywords: [ - 'curl', - 'document', - 'page', - 'uc6', - 'classroom', - 'write', - 'document', - 'envelope', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'writing', - 'documents', - 'letter', - 'message', - 'offer', - 'office' - ]), - Emoji( - name: 'page facing up', - char: '\u{1F4C4}', - shortName: 'page_facing_up', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.bookPaper, - keywords: [ - 'document', - 'page', - 'uc6', - 'classroom', - 'write', - 'document', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'writing', - 'documents', - 'office' - ]), - Emoji( - name: 'bookmark tabs', - char: '\u{1F4D1}', - shortName: 'bookmark_tabs', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.bookPaper, - keywords: [ - 'bookmark', - 'mark', - 'marker', - 'tabs', - 'uc6', - 'classroom', - 'write', - 'document', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'writing', - 'documents', - 'office' - ]), - Emoji( - name: 'receipt', - char: '\u{1F9FE}', - shortName: 'receipt', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.money, - keywords: [ - 'uc11', - 'document', - 'business', - 'discount', - 'history', - 'price', - 'rich', - 'purchase', - 'household', - 'restaurant', - 'invoice', - 'documents', - 'sale', - 'bargain', - 'ancient', - 'old', - 'grand', - 'expensive', - 'fancy', - 'buy', - 'shop', - 'spend' - ]), - Emoji( - name: 'bar chart', - char: '\u{1F4CA}', - shortName: 'bar_chart', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.office, - keywords: [ - 'bar', - 'chart', - 'graph', - 'uc6', - 'classroom', - 'business', - 'data', - 'measure', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'graph', - 'office' - ]), - Emoji( - name: 'chart increasing', - char: '\u{1F4C8}', - shortName: 'chart_with_upwards_trend', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.office, - keywords: [ - 'chart', - 'graph', - 'growth', - 'trend', - 'upward', - 'uc6', - 'classroom', - 'business', - 'data', - 'measure', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'graph', - 'office' - ]), - Emoji( - name: 'chart decreasing', - char: '\u{1F4C9}', - shortName: 'chart_with_downwards_trend', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.office, - keywords: [ - 'chart', - 'down', - 'graph', - 'trend', - 'uc6', - 'classroom', - 'business', - 'data', - 'measure', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'graph', - 'office' - ]), - Emoji( - name: 'spiral notepad', - char: '\u{1F5D2}\u{FE0F}', - shortName: 'notepad_spiral', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.office, - keywords: [ - 'note', - 'pad', - 'spiral', - 'uc7', - 'classroom', - 'write', - 'business', - 'envelope', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'writing', - 'letter', - 'message', - 'offer', - 'office' - ]), - Emoji( - name: 'spiral calendar', - char: '\u{1F5D3}\u{FE0F}', - shortName: 'calendar_spiral', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.office, - keywords: [ - 'calendar', - 'pad', - 'spiral', - 'uc7', - 'classroom', - 'calendar', - 'advent', - 'schedule', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'date', - 'sunday', - 'monday', - 'tuesday', - 'wednesday', - 'thursday', - 'friday', - 'saturday', - 'month', - 'agenda', - 'mois', - 'year', - 'today', - 'jour', - 'week', - 'when', - 'office' - ]), - Emoji( - name: 'tear-off calendar', - char: '\u{1F4C6}', - shortName: 'calendar', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.office, - keywords: [ - 'calendar', - 'uc6', - 'classroom', - 'day', - 'calendar', - 'business', - 'schedule', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'date', - 'sunday', - 'monday', - 'tuesday', - 'wednesday', - 'thursday', - 'friday', - 'saturday', - 'month', - 'agenda', - 'mois', - 'year', - 'today', - 'jour', - 'week', - 'when', - 'office' - ]), - Emoji( - name: 'calendar', - char: '\u{1F4C5}', - shortName: 'date', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.office, - keywords: [ - 'date', - 'uc6', - 'classroom', - 'calendar', - 'advent', - 'schedule', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'date', - 'sunday', - 'monday', - 'tuesday', - 'wednesday', - 'thursday', - 'friday', - 'saturday', - 'month', - 'agenda', - 'mois', - 'year', - 'today', - 'jour', - 'week', - 'when', - 'office' - ]), - Emoji( - name: 'wastebasket', - char: '\u{1F5D1}\u{FE0F}', - shortName: 'wastebasket', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.office, - keywords: [ - 'wastebasket', - 'uc7', - 'classroom', - 'business', - 'trash', - 'clean', - 'empty', - 'delete', - 'household', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'litter', - 'trash can', - 'garbage', - 'rubbish', - 'poubelle', - 'basura', - 'office' - ]), - Emoji( - name: 'card index', - char: '\u{1F4C7}', - shortName: 'card_index', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.office, - keywords: [ - 'card', - 'index', - 'rolodex', - 'uc6', - 'classroom', - 'business', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'office' - ]), - Emoji( - name: 'card file box', - char: '\u{1F5C3}\u{FE0F}', - shortName: 'card_box', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.office, - keywords: [ - 'box', - 'card', - 'file', - 'uc7', - 'classroom', - 'business', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'office' - ]), - Emoji( - name: 'ballot box with ballot', - char: '\u{1F5F3}\u{FE0F}', - shortName: 'ballot_box', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.mail, - keywords: [ - 'ballot', - 'box', - 'uc7', - 'classroom', - 'vote', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning' - ]), - Emoji( - name: 'file cabinet', - char: '\u{1F5C4}\u{FE0F}', - shortName: 'file_cabinet', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.office, - keywords: [ - 'cabinet', - 'file', - 'filing', - 'uc7', - 'classroom', - 'business', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'office' - ]), - Emoji( - name: 'clipboard', - char: '\u{1F4CB}', - shortName: 'clipboard', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.office, - keywords: [ - 'clipboard', - 'uc6', - 'classroom', - 'write', - 'business', - 'data', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'writing', - 'graph', - 'office' - ]), - Emoji( - name: 'file folder', - char: '\u{1F4C1}', - shortName: 'file_folder', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.office, - keywords: [ - 'file', - 'folder', - 'uc6', - 'classroom', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'office' - ]), - Emoji( - name: 'open file folder', - char: '\u{1F4C2}', - shortName: 'open_file_folder', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.office, - keywords: [ - 'file', - 'folder', - 'open', - 'uc6', - 'classroom', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'office' - ]), - Emoji( - name: 'card index dividers', - char: '\u{1F5C2}\u{FE0F}', - shortName: 'dividers', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.office, - keywords: [ - 'card', - 'dividers', - 'index', - 'uc7', - 'classroom', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'office' - ]), - Emoji( - name: 'rolled-up newspaper', - char: '\u{1F5DE}\u{FE0F}', - shortName: 'newspaper2', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.bookPaper, - keywords: [ - 'news', - 'newspaper', - 'paper', - 'rolled', - 'uc7', - 'classroom', - 'write', - 'news', - 'history', - 'household', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'writing', - 'article', - 'ancient', - 'old', - 'office' - ]), - Emoji( - name: 'newspaper', - char: '\u{1F4F0}', - shortName: 'newspaper', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.bookPaper, - keywords: [ - 'news', - 'paper', - 'uc6', - 'classroom', - 'write', - 'news', - 'history', - 'household', - 'work', - 'covid', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'writing', - 'article', - 'ancient', - 'old', - 'office' - ]), - Emoji( - name: 'notebook', - char: '\u{1F4D3}', - shortName: 'notebook', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.bookPaper, - keywords: [ - 'notebook', - 'uc6', - 'book', - 'classroom', - 'write', - 'work', - 'journal', - 'books', - 'read', - 'reading', - 'cahier', - 'livre', - 'cuaderno', - 'diary', - 'dictionary', - 'encyclopedia', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'writing', - 'office' - ]), - Emoji( - name: 'notebook with decorative cover', - char: '\u{1F4D4}', - shortName: 'notebook_with_decorative_cover', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.bookPaper, - keywords: [ - 'book', - 'cover', - 'decorated', - 'notebook', - 'uc6', - 'book', - 'classroom', - 'write', - 'work', - 'journal', - 'books', - 'read', - 'reading', - 'cahier', - 'livre', - 'cuaderno', - 'diary', - 'dictionary', - 'encyclopedia', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'writing', - 'office' - ]), - Emoji( - name: 'ledger', - char: '\u{1F4D2}', - shortName: 'ledger', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.bookPaper, - keywords: [ - 'notebook', - 'uc6', - 'classroom', - 'write', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'writing', - 'office' - ]), - Emoji( - name: 'closed book', - char: '\u{1F4D5}', - shortName: 'closed_book', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.bookPaper, - keywords: [ - 'book', - 'closed', - 'uc6', - 'book', - 'classroom', - 'write', - 'bible', - 'history', - 'work', - 'books', - 'read', - 'reading', - 'cahier', - 'livre', - 'cuaderno', - 'diary', - 'dictionary', - 'encyclopedia', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'writing', - 'ancient', - 'old', - 'office' - ]), - Emoji( - name: 'green book', - char: '\u{1F4D7}', - shortName: 'green_book', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.bookPaper, - keywords: [ - 'book', - 'green', - 'uc6', - 'book', - 'classroom', - 'bible', - 'history', - 'work', - 'books', - 'read', - 'reading', - 'cahier', - 'livre', - 'cuaderno', - 'diary', - 'dictionary', - 'encyclopedia', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'ancient', - 'old', - 'office' - ]), - Emoji( - name: 'blue book', - char: '\u{1F4D8}', - shortName: 'blue_book', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.bookPaper, - keywords: [ - 'blue', - 'book', - 'uc6', - 'book', - 'classroom', - 'write', - 'bible', - 'history', - 'work', - 'books', - 'read', - 'reading', - 'cahier', - 'livre', - 'cuaderno', - 'diary', - 'dictionary', - 'encyclopedia', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'writing', - 'ancient', - 'old', - 'office' - ]), - Emoji( - name: 'orange book', - char: '\u{1F4D9}', - shortName: 'orange_book', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.bookPaper, - keywords: [ - 'book', - 'orange', - 'uc6', - 'book', - 'classroom', - 'write', - 'bible', - 'history', - 'work', - 'books', - 'read', - 'reading', - 'cahier', - 'livre', - 'cuaderno', - 'diary', - 'dictionary', - 'encyclopedia', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'writing', - 'ancient', - 'old', - 'office' - ]), - Emoji( - name: 'books', - char: '\u{1F4DA}', - shortName: 'books', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.bookPaper, - keywords: [ - 'book', - 'uc6', - 'book', - 'classroom', - 'write', - 'harry potter', - 'nerd', - 'history', - 'work', - 'books', - 'read', - 'reading', - 'cahier', - 'livre', - 'cuaderno', - 'diary', - 'dictionary', - 'encyclopedia', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'writing', - 'smart', - 'geek', - 'serious', - 'ancient', - 'old', - 'office' - ]), - Emoji( - name: 'open book', - char: '\u{1F4D6}', - shortName: 'book', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.bookPaper, - keywords: [ - 'book', - 'open', - 'uc6', - 'book', - 'classroom', - 'write', - 'harry potter', - 'history', - 'schedule', - 'work', - 'journal', - 'books', - 'read', - 'reading', - 'cahier', - 'livre', - 'cuaderno', - 'diary', - 'dictionary', - 'encyclopedia', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'writing', - 'ancient', - 'old', - 'office' - ]), - Emoji( - name: 'bookmark', - char: '\u{1F516}', - shortName: 'bookmark', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.bookPaper, - keywords: [ - 'mark', - 'uc6', - 'book', - 'discount', - 'price', - 'household', - 'books', - 'read', - 'reading', - 'cahier', - 'livre', - 'cuaderno', - 'diary', - 'dictionary', - 'encyclopedia', - 'sale', - 'bargain' - ]), - Emoji( - name: 'safety pin', - char: '\u{1F9F7}', - shortName: 'safety_pin', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.household, - keywords: [ - 'uc11', - 'household', - 'sew', - 'work', - 'knit', - 'embroider', - 'stitch', - 'repair', - 'crochet', - 'alter', - 'seamstress', - 'fix', - 'office' - ]), - Emoji( - name: 'link', - char: '\u{1F517}', - shortName: 'link', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.tool, - keywords: [ - 'link', - 'uc6', - 'classroom', - 'steel', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'metal' - ]), - Emoji( - name: 'paperclip', - char: '\u{1F4CE}', - shortName: 'paperclip', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.office, - keywords: [ - 'paperclip', - 'uc6', - 'classroom', - 'business', - 'household', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'office' - ]), - Emoji( - name: 'linked paperclips', - char: '\u{1F587}\u{FE0F}', - shortName: 'paperclips', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.office, - keywords: [ - 'link', - 'paperclip', - 'uc7', - 'classroom', - 'business', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'office' - ]), - Emoji( - name: 'triangular ruler', - char: '\u{1F4D0}', - shortName: 'triangular_ruler', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.office, - keywords: [ - 'ruler', - 'set', - 'triangle', - 'uc6', - 'tool', - 'classroom', - 'triangle', - 'measure', - 'work', - 'tools', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'triangles', - 'office' - ]), - Emoji( - name: 'straight ruler', - char: '\u{1F4CF}', - shortName: 'straight_ruler', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.office, - keywords: [ - 'ruler', - 'straight edge', - 'uc6', - 'theatre', - 'tool', - 'classroom', - 'household', - 'measure', - 'work', - 'theater', - 'craft', - 'drama', - 'monet', - 'tools', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'office' - ]), - Emoji( - name: 'abacus', - char: '\u{1F9EE}', - shortName: 'abacus', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.computer, - keywords: [ - 'uc11', - 'math', - 'science', - 'vintage', - 'history', - 'calculator', - 'toy', - 'work', - 'decimal', - 'percentage', - 'fraction', - 'lab', - 'ancient', - 'old', - 'count', - 'add', - 'office' - ]), - Emoji( - name: 'pushpin', - char: '\u{1F4CC}', - shortName: 'pushpin', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.office, - keywords: [ - 'pin', - 'uc6', - 'classroom', - 'map', - 'business', - 'household', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'maps', - 'location', - 'locate', - 'local', - 'lost', - 'office' - ]), - Emoji( - name: 'round pushpin', - char: '\u{1F4CD}', - shortName: 'round_pushpin', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.office, - keywords: [ - 'pin', - 'pushpin', - 'uc6', - 'classroom', - 'map', - 'business', - 'household', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'maps', - 'location', - 'locate', - 'local', - 'lost', - 'office' - ]), - Emoji( - name: 'scissors', - char: '\u{2702}\u{FE0F}', - shortName: 'scissors', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.office, - keywords: [ - 'cutting', - 'tool', - 'uc1', - 'theatre', - 'tool', - 'weapon', - 'classroom', - 'steel', - 'household', - 'sew', - 'work', - 'theater', - 'craft', - 'drama', - 'monet', - 'tools', - 'weapons', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'metal', - 'knit', - 'embroider', - 'stitch', - 'repair', - 'crochet', - 'alter', - 'seamstress', - 'fix', - 'office' - ]), - Emoji( - name: 'pen', - char: '\u{1F58A}\u{FE0F}', - shortName: 'pen_ballpoint', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.writing, - keywords: [ - 'ballpoint', - 'uc7', - 'tool', - 'classroom', - 'write', - 'business', - 'color', - 'correct', - 'detective', - 'household', - 'work', - 'journal', - 'tools', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'writing', - 'colour', - 'coloring', - 'colouring', - 'drawing', - 'marker', - 'sketch', - 'passing grade', - 'office' - ]), - Emoji( - name: 'fountain pen', - char: '\u{1F58B}\u{FE0F}', - shortName: 'pen_fountain', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.writing, - keywords: [ - 'fountain', - 'pen', - 'uc7', - 'tool', - 'classroom', - 'write', - 'color', - 'correct', - 'work', - 'tools', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'writing', - 'colour', - 'coloring', - 'colouring', - 'drawing', - 'marker', - 'sketch', - 'passing grade', - 'office' - ]), - Emoji( - name: 'black nib', - char: '\u{2712}\u{FE0F}', - shortName: 'black_nib', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.writing, - keywords: [ - 'nib', - 'pen', - 'uc1', - 'classroom', - 'write', - 'correct', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'writing', - 'passing grade', - 'office' - ]), - Emoji( - name: 'paintbrush', - char: '\u{1F58C}\u{FE0F}', - shortName: 'paintbrush', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.writing, - keywords: [ - 'painting', - 'uc7', - 'theatre', - 'classroom', - 'write', - 'painting', - 'color', - 'theater', - 'craft', - 'drama', - 'monet', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'writing', - 'painter', - 'arts', - 'colour', - 'coloring', - 'colouring', - 'drawing', - 'marker', - 'sketch' - ]), - Emoji( - name: 'crayon', - char: '\u{1F58D}\u{FE0F}', - shortName: 'crayon', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.writing, - keywords: [ - 'crayon', - 'uc7', - 'theatre', - 'classroom', - 'write', - 'color', - 'household', - 'theater', - 'craft', - 'drama', - 'monet', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'writing', - 'colour', - 'coloring', - 'colouring', - 'drawing', - 'marker', - 'sketch' - ]), - Emoji( - name: 'memo', - char: '\u{1F4DD}', - shortName: 'pencil', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.writing, - keywords: [ - 'pencil', - 'uc6', - 'classroom', - 'write', - 'document', - 'envelope', - 'color', - 'correct', - 'work', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'writing', - 'documents', - 'letter', - 'message', - 'offer', - 'colour', - 'coloring', - 'colouring', - 'drawing', - 'marker', - 'sketch', - 'passing grade', - 'office' - ]), - Emoji( - name: 'pencil', - char: '\u{270F}\u{FE0F}', - shortName: 'pencil2', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.writing, - keywords: [ - 'pencil', - 'uc1', - 'theatre', - 'tool', - 'classroom', - 'write', - 'color', - 'correct', - 'household', - 'work', - 'theater', - 'craft', - 'drama', - 'monet', - 'tools', - 'school', - 'teach', - 'learn', - 'study', - 'college', - 'degree', - 'education', - 'homework', - 'student', - 'teacher', - 'university', - 'test', - 'learning', - 'writing', - 'colour', - 'coloring', - 'colouring', - 'drawing', - 'marker', - 'sketch', - 'passing grade', - 'office' - ]), - Emoji( - name: 'magnifying glass tilted left', - char: '\u{1F50D}', - shortName: 'mag', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.lightVideo, - keywords: [ - 'glass', - 'magnifying', - 'search', - 'tool', - 'uc6', - 'google', - 'search', - 'detective', - 'household', - 'look', - 'find', - 'looking', - 'see' - ]), - Emoji( - name: 'magnifying glass tilted right', - char: '\u{1F50E}', - shortName: 'mag_right', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.lightVideo, - keywords: [ - 'glass', - 'magnifying', - 'search', - 'tool', - 'uc6', - 'google', - 'search', - 'detective', - 'look', - 'find', - 'looking', - 'see' - ]), - Emoji( - name: 'locked with pen', - char: '\u{1F50F}', - shortName: 'lock_with_ink_pen', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.lock, - keywords: [ - 'ink', - 'lock', - 'nib', - 'pen', - 'privacy', - 'uc6', - 'lock', - 'locks', - 'key', - 'keys' - ]), - Emoji( - name: 'locked with key', - char: '\u{1F510}', - shortName: 'closed_lock_with_key', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.lock, - keywords: [ - 'closed', - 'key', - 'lock', - 'secure', - 'uc6', - 'lock', - 'household', - 'locks', - 'key', - 'keys' - ]), - Emoji( - name: 'locked', - char: '\u{1F512}', - shortName: 'lock', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.lock, - keywords: ['closed', 'uc6', 'lock', 'locks', 'key', 'keys']), - Emoji( - name: 'unlocked', - char: '\u{1F513}', - shortName: 'unlock', - emojiGroup: EmojiGroup.objects, - emojiSubgroup: EmojiSubgroup.lock, - keywords: [ - 'lock', - 'open', - 'unlock', - 'uc6', - 'lock', - 'locks', - 'key', - 'keys' - ]), - Emoji( - name: 'red heart', - char: '\u{2764}\u{FE0F}', - shortName: 'heart', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.emotion, - keywords: [ - 'heart', - 'uc1', - 'shapes', - 'love', - 'rainbow', - 'red heart', - 'heart', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'hearts', - 'serce', - 'corazón', - 'coração', - 'coeur', - '<3' - ]), - Emoji( - name: 'orange heart', - char: '\u{1F9E1}', - shortName: 'orange_heart', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.emotion, - keywords: [ - 'orange', - 'uc10', - 'shapes', - 'love', - 'rainbow', - 'orange', - 'heart', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'hearts', - 'serce', - 'corazón', - 'coração', - 'coeur' - ]), - Emoji( - name: 'yellow heart', - char: '\u{1F49B}', - shortName: 'yellow_heart', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.emotion, - keywords: [ - 'yellow', - 'uc6', - 'shapes', - 'love', - 'rainbow', - 'friend', - 'heart', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'hearts', - 'serce', - 'corazón', - 'coração', - 'coeur' - ]), - Emoji( - name: 'green heart', - char: '\u{1F49A}', - shortName: 'green_heart', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.emotion, - keywords: [ - 'green', - 'uc6', - 'shapes', - 'halloween', - 'love', - 'rainbow', - 'irish', - 'jealous', - 'heart', - 'samhain', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'saint patricks day', - 'st patricks day', - 'leprechaun', - 'hearts', - 'serce', - 'corazón', - 'coração', - 'coeur' - ]), - Emoji( - name: 'blue heart', - char: '\u{1F499}', - shortName: 'blue_heart', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.emotion, - keywords: [ - 'blue', - 'uc6', - 'shapes', - 'love', - 'rainbow', - 'friend', - 'heart', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'friends', - 'friendship', - 'best friends', - 'bestfriends', - 'ami', - 'amiga', - 'amigo', - 'hearts', - 'serce', - 'corazón', - 'coração', - 'coeur' - ]), - Emoji( - name: 'purple heart', - char: '\u{1F49C}', - shortName: 'purple_heart', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.emotion, - keywords: [ - 'purple', - 'uc6', - 'shapes', - 'love', - 'rainbow', - 'pink', - 'heart', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'rose', - 'hearts', - 'serce', - 'corazón', - 'coração', - 'coeur' - ]), - Emoji( - name: 'black heart', - char: '\u{1F5A4}', - shortName: 'black_heart', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.emotion, - keywords: [ - 'black', - 'evil', - 'wicked', - 'uc9', - 'shapes', - 'halloween', - 'love', - 'heartbreak', - 'rainbow', - 'hate', - 'killer', - 'heart', - 'samhain', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'broken heart', - 'heartbroken', - 'i hate', - 'disgust', - 'stump', - 'shout', - 'dislike', - 'rude', - 'annoy', - 'grinch', - 'gross', - 'grumpy', - 'mean', - 'problem', - 'suck', - 'jerk', - 'asshole', - 'no', - 'savage', - 'scary clown', - 'hearts', - 'serce', - 'corazón', - 'coração', - 'coeur' - ]), - Emoji( - name: 'brown heart', - char: '\u{1F90E}', - shortName: 'brown_heart', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.emotion, - keywords: [ - 'uc12', - 'shapes', - 'heart', - 'hearts', - 'serce', - 'corazón', - 'coração', - 'coeur' - ]), - Emoji( - name: 'white heart', - char: '\u{1F90D}', - shortName: 'white_heart', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.emotion, - keywords: [ - 'uc12', - 'shapes', - 'heart', - 'hearts', - 'serce', - 'corazón', - 'coração', - 'coeur' - ]), - Emoji( - name: 'broken heart', - char: '\u{1F494}', - shortName: 'broken_heart', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.emotion, - keywords: [ - 'break', - 'broken', - 'uc6', - 'love', - 'heartbreak', - 'red heart', - 'heart', - 'i love you', - 'te amo', - "je t'aime", - 'anniversary', - 'lovin', - 'amour', - 'aimer', - 'amor', - 'valentines day', - 'enamour', - 'lovey', - 'broken heart', - 'heartbroken', - 'hearts', - 'serce', - 'corazón', - 'coração', - 'coeur', - ' keywords; - List? _runes; - - /// Emoji class. - /// [name] of emoji. [char] and character of emoji. [shortName] and a digest name of emoji, [emojiGroup] is emoji's group and [emojiSubgroup] is emoji's subgroup. [keywords] list of keywords for emoji. [modifiable] `true` if emoji has skin. - Emoji({ - this.name, - this.char, - this.shortName, - this.emojiGroup, - this.emojiSubgroup, - this.keywords = const [], - this.modifiable = false, - }); - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Emoji && - runtimeType == other.runtimeType && - name == other.name && - char == other.char && - shortName == other.shortName && - emojiGroup == other.emojiGroup && - emojiSubgroup == other.emojiSubgroup && - const ListEquality().equals(keywords, other.keywords) && - modifiable == other.modifiable; - - @override - int get hashCode => Object.hash( - name.hashCode, - char.hashCode, - shortName.hashCode, - emojiGroup.hashCode, - emojiSubgroup.hashCode, - keywords.hashCode, - modifiable.hashCode, - ); - - /// Runes of Emoji Character - List get charRunes { - return _runes ??= char!.runes.toList(); - } - - /// Returns current Emoji with New requested [skinTone] if modifiable, else Returns current Emoji - Emoji? newSkin(fitzpatrick skinTone) { - if (modifiable) { - switch (skinTone) { - case fitzpatrick.light: - return Emoji( - name: this.name! + ', tone1', - char: modify(this.char, skinTone), - shortName: this.shortName! + '_tone1', - emojiGroup: this.emojiGroup, - emojiSubgroup: this.emojiSubgroup, - keywords: this.keywords, - modifiable: true); - case fitzpatrick.mediumLight: - return Emoji( - name: this.name! + ', tone2', - char: modify(this.char, skinTone), - shortName: this.shortName! + '_tone2', - emojiGroup: this.emojiGroup, - emojiSubgroup: this.emojiSubgroup, - keywords: this.keywords, - modifiable: true); - case fitzpatrick.medium: - return Emoji( - name: this.name! + ', tone3', - char: modify(this.char, skinTone), - shortName: this.shortName! + '_tone3', - emojiGroup: this.emojiGroup, - emojiSubgroup: this.emojiSubgroup, - keywords: this.keywords, - modifiable: true); - case fitzpatrick.mediumDark: - return Emoji( - name: this.name! + ', tone4', - char: modify(this.char, skinTone), - shortName: this.shortName! + '_tone4', - emojiGroup: this.emojiGroup, - emojiSubgroup: this.emojiSubgroup, - keywords: this.keywords, - modifiable: true); - case fitzpatrick.dark: - return Emoji( - name: this.name! + ', tone5', - char: modify(this.char, skinTone), - shortName: this.shortName! + '_tone5', - emojiGroup: this.emojiGroup, - emojiSubgroup: this.emojiSubgroup, - keywords: this.keywords, - modifiable: true); - case fitzpatrick.None: - return Emoji.byChar(stabilize(this.char)); - } - } - return this; - } - - /// Get all Emojis - static List all() => List.unmodifiable(_emojis); - - static Iterable chars() => - _emojis.map((e) => e.char).whereType(); - - /// Returns Emoji by [char] and character - static Emoji? byChar(String char) { - return _emojis.firstWhereOrNull((Emoji emoji) => emoji.char == char); - } - - /// Returns Emoji by [name] - static Emoji? byName(String name) { - name = name.toLowerCase(); // todo: searchable name - return _emojis.firstWhereOrNull((Emoji emoji) => emoji.name == name); - } - - /// Returns Emoji by [shortName] as short name. - static Emoji? byShortName(String shortName) { - return _emojis.firstWhereOrNull( - (Emoji emoji) => emoji.shortName == shortName, - ); - } - - /// Returns list of Emojis in a same [group] - static Iterable byGroup(EmojiGroup group) { - return _emojis.where((Emoji emoji) => emoji.emojiGroup == group); - } - - /// Returns list of Emojis in a same [subgroup] - static Iterable bySubgroup(EmojiSubgroup subgroup) { - return _emojis.where((Emoji emoji) => emoji.emojiSubgroup == subgroup); - } - - /// Returns List of Emojis with Specific [keyword] - static Iterable byKeyword(String keyword) { - keyword = keyword.toLowerCase(); - return _emojis.where((Emoji emoji) => emoji.keywords.contains(keyword)); - } - - /// disassemble [emoji] to list of emojis, without skin tones if [noSkin] be `true`. - static List disassemble(String emoji, {bool noSkin = false}) { - List emojiRunes = emoji.runes.toList(); - emojiRunes.removeWhere((codeChar) => - ZeroWidthCharCodes.contains(codeChar) || - (noSkin && _isFitzpatrickCode(codeChar))); - return emojiRunes.map((char) => String.fromCharCode(char)).toList(); - // return emoji.runes.toList()..removeWhere((codeChar) => ZeroWidthCharCodes.contains(codeChar) || (noSkin && _isFitzpatrickCode(codeChar))).map((char) => String.fromCharCode(char)).toList() - } - - /// assemble emojis with [emojiChars] codes. - static String assemble(List emojiChars) { - List codeCharPoints = []; - - for (var i = 0; i < emojiChars.length; i++) { - if (i != 0 && !isFitzpatrick(emojiChars[i - 1])) { - codeCharPoints.add(ZWJ); - } - final emojiRunes = emojiChars[i].runes.toList(); - codeCharPoints.addAll(emojiRunes); - } - codeCharPoints.add(variationSelector16); - return String.fromCharCodes(codeCharPoints); - } - - /// Modify skin tone of [emoji] by requested [skinTone] - static String modify(String? emoji, fitzpatrick skinTone) { - int? skinToneCharCode; - switch (skinTone) { - case fitzpatrick.light: - skinToneCharCode = 127995; - break; - case fitzpatrick.mediumLight: - skinToneCharCode = 127996; - break; - case fitzpatrick.medium: - skinToneCharCode = 127997; - break; - case fitzpatrick.mediumDark: - skinToneCharCode = 127998; - break; - case fitzpatrick.dark: - skinToneCharCode = 127999; - break; - case fitzpatrick.None: - return stabilize(emoji); - } - - final emojiRunes = emoji!.runes.toList(); - List finalCharCodes = []; - for (final charCode in emojiRunes) { - if (!_isFitzpatrickCode(charCode)) { - finalCharCodes.add(charCode); - if (_isModifiable(charCode)) { - finalCharCodes.add(skinToneCharCode); - } - } - } - return String.fromCharCodes(finalCharCodes as Iterable); - } - - // todo: support unspecified gender for "... holding hands", "kiss", "couple with heart" and "family". - /// stabilize [skin] and [gender] of [emoji], if `true`. - static String stabilize(String? emoji, - {bool skin = true, bool gender = false}) { - if (gender) { - emoji = emoji! - .replaceAll( - '\u{200D}\u{2642}\u{FE0F}', '') // remove ZWJ man from emoji - .replaceAll( - '\u{200D}\u{2640}\u{FE0F}', '') // remove ZWJ woman from emoji - .replaceAll('\u{1F468}', '\u{1F9D1}') // replace man with person - .replaceAll('\u{1F469}', '\u{1F9D1}') // replace woman with person - .replaceAll( - '\u{1F474}', '\u{1F9D3}') // replace old man with old person - .replaceAll( - '\u{1F475}', '\u{1F9D3}'); // replace old woman with old person - } - - final List emojiRunes = emoji!.runes.toList(); - - if (skin) { - emojiRunes.removeWhere((codeChar) => _isFitzpatrickCode(codeChar)); - } - return String.fromCharCodes(emojiRunes); - } - - /// returns `true` if [emojiCode] is code of Emoji with skin!. - static _isModifiable(int emojiCode) { - return _modifiableCharCodes.contains(emojiCode); - } - - /// returns `true` if [emoji] is a Fitzpatrick Emoji. - static bool isFitzpatrick(String emoji) { - return skinToneEmojiChars.contains(emoji); - } - - /// returns `true` if [emojiCode] is code of Fitzpatrick Emoji. - static bool _isFitzpatrickCode(int emojiCode) { - return _skinToneCharCodes.contains(emojiCode); - } - - @override - toString() => char!; -} diff --git a/packages/stream_chat_flutter/lib/src/emoji_overlay.dart b/packages/stream_chat_flutter/lib/src/emoji_overlay.dart deleted file mode 100644 index 04af4179..00000000 --- a/packages/stream_chat_flutter/lib/src/emoji_overlay.dart +++ /dev/null @@ -1,124 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/emoji/emoji.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:substring_highlight/substring_highlight.dart'; - -/// {@macro emoji_overlay} -@Deprecated("Use 'StreamEmojiOverlay' instead") -typedef EmojiOverlay = StreamEmojiOverlay; - -/// {@template emoji_overlay} -/// Overlay for displaying emoji that can be used -/// {@endtemplate} -class StreamEmojiOverlay extends StatelessWidget { - /// Constructor for creating a [StreamEmojiOverlay] - const StreamEmojiOverlay({ - required this.query, - required this.onEmojiResult, - required this.size, - super.key, - }); - - /// The size of the overlay - final Size size; - - /// Query for searching emoji - final String query; - - /// Callback called when an emoji is selected - final ValueChanged onEmojiResult; - - @override - Widget build(BuildContext context) { - final _streamChatTheme = StreamChatTheme.of(context); - final _emojiNames = - Emoji.all().where((it) => it.name != null).map((e) => e.name!); - - final emojis = _emojiNames - .where((e) => e.contains(query)) - .map(Emoji.byName) - .where((e) => e != null); - - if (emojis.isEmpty) { - return const SizedBox(); - } - - return Card( - margin: const EdgeInsets.all(8), - elevation: 2, - color: _streamChatTheme.colorTheme.barsBg, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - clipBehavior: Clip.hardEdge, - child: Container( - constraints: BoxConstraints.loose(size), - decoration: BoxDecoration( - boxShadow: const [ - BoxShadow( - spreadRadius: -8, - blurRadius: 5, - offset: Offset(0, -4), - ), - ], - color: _streamChatTheme.colorTheme.barsBg, - ), - child: ListView.builder( - padding: EdgeInsets.zero, - shrinkWrap: true, - itemCount: emojis.length + 1, - itemBuilder: (context, i) { - if (i == 0) { - return Padding( - padding: const EdgeInsets.only(left: 8, top: 8), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: StreamSvgIcon.smile( - color: _streamChatTheme.colorTheme.accentPrimary, - ), - ), - Flexible( - child: Text( - context.translations.emojiMatchingQueryText( - query, - ), - style: TextStyle( - color: _streamChatTheme.colorTheme.textHighEmphasis - .withOpacity(0.5), - ), - ), - ), - ], - ), - ); - } - - final emoji = emojis.elementAt(i - 1)!; - final themeData = Theme.of(context); - return ListTile( - title: SubstringHighlight( - text: - // ignore: lines_longer_than_80_chars - "${emoji.char} ${emoji.name!.replaceAll('_', ' ')}", - term: query, - textStyleHighlight: themeData.textTheme.headline6!.copyWith( - fontSize: 14.5, - fontWeight: FontWeight.bold, - ), - textStyle: themeData.textTheme.headline6!.copyWith( - fontSize: 14.5, - ), - ), - onTap: () { - onEmojiResult(emoji); - }, - ); - }, - ), - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/full_screen_media.dart b/packages/stream_chat_flutter/lib/src/full_screen_media.dart deleted file mode 100644 index 55120de0..00000000 --- a/packages/stream_chat_flutter/lib/src/full_screen_media.dart +++ /dev/null @@ -1,353 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:chewie/chewie.dart'; -import 'package:flutter/material.dart'; -import 'package:photo_view/photo_view.dart'; -import 'package:shimmer/shimmer.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:video_player/video_player.dart'; - -/// Return action for coming back from pages -enum ReturnActionType { - /// No return action - none, - - /// Go to reply message action - reply, -} - -/// Callback when show message is tapped -typedef ShowMessageCallback = void Function(Message message, Channel channel); - -/// {@macro full_screen_media} -@Deprecated("Use 'StreamFullScreenMedia' instead") -typedef FullScreenMedia = StreamFullScreenMedia; - -/// {@template full_screen_media} -/// A full screen image widget -/// {@endtemplate} -class StreamFullScreenMedia extends StatefulWidget { - /// Instantiate a new FullScreenImage - const StreamFullScreenMedia({ - super.key, - required this.mediaAttachmentPackages, - this.startIndex = 0, - String? userName, - this.onShowMessage, - this.attachmentActionsModalBuilder, - this.autoplayVideos = false, - }) : userName = userName ?? ''; - - /// The url of the image - final List mediaAttachmentPackages; - - /// First index of media shown - final int startIndex; - - /// Username of sender - final String userName; - - /// Callback for when show message is tapped - final ShowMessageCallback? onShowMessage; - - /// Widget builder for attachment actions modal - /// [defaultActionsModal] is the default [AttachmentActionsModal] config - /// Use [defaultActionsModal.copyWith] to easily customize it - final AttachmentActionsBuilder? attachmentActionsModalBuilder; - - /// Auto-play videos when page is opened - final bool autoplayVideos; - - @override - _StreamFullScreenMediaState createState() => _StreamFullScreenMediaState(); -} - -class _StreamFullScreenMediaState extends State { - late final PageController _pageController; - - late final _currentPage = ValueNotifier(widget.startIndex); - late final _isDisplayingDetail = ValueNotifier(true); - - void switchDisplayingDetail() { - _isDisplayingDetail.value = !_isDisplayingDetail.value; - } - - final videoPackages = {}; - - @override - void initState() { - super.initState(); - _pageController = PageController(initialPage: widget.startIndex); - for (var i = 0; i < widget.mediaAttachmentPackages.length; i++) { - final attachment = widget.mediaAttachmentPackages[i].attachment; - if (attachment.type != 'video') continue; - final package = VideoPackage(attachment, showControls: true); - videoPackages[attachment.id] = package; - } - initializePlayers(); - } - - Future initializePlayers() async { - if (videoPackages.isEmpty) { - return; - } - - final currentAttachment = - widget.mediaAttachmentPackages[widget.startIndex].attachment; - - await Future.wait(videoPackages.values.map( - (it) => it.initialize(), - )); - - if (widget.autoplayVideos && currentAttachment.type == 'video') { - final package = videoPackages.values - .firstWhere((e) => e._attachment == currentAttachment); - package._chewieController?.play(); - } - setState(() {}); // ignore: no-empty-block - } - - @override - Widget build(BuildContext context) => Scaffold( - resizeToAvoidBottomInset: false, - body: ValueListenableBuilder( - valueListenable: _currentPage, - builder: (context, currentPage, child) { - final _currentAttachmentPackage = - widget.mediaAttachmentPackages[currentPage]; - final _currentMessage = _currentAttachmentPackage.message; - final _currentAttachment = _currentAttachmentPackage.attachment; - return Stack( - children: [ - Positioned.fill(child: child!), - ValueListenableBuilder( - valueListenable: _isDisplayingDetail, - builder: (context, isDisplayingDetail, child) { - final mediaQuery = MediaQuery.of(context); - final topPadding = mediaQuery.padding.top; - return AnimatedPositionedDirectional( - duration: kThemeAnimationDuration, - curve: Curves.easeInOut, - top: isDisplayingDetail - ? 0 - : -(topPadding + kToolbarHeight), - start: 0, - end: 0, - height: topPadding + kToolbarHeight, - child: StreamGalleryHeader( - userName: widget.userName, - sentAt: context.translations.sentAtText( - date: _currentAttachmentPackage.message.createdAt, - time: _currentAttachmentPackage.message.createdAt, - ), - onBackPressed: Navigator.of(context).pop, - message: _currentMessage, - attachment: _currentAttachment, - onShowMessage: () { - widget.onShowMessage?.call( - _currentMessage, - StreamChannel.of(context).channel, - ); - }, - attachmentActionsModalBuilder: - widget.attachmentActionsModalBuilder, - ), - ); - }, - ), - if (!_currentMessage.isEphemeral) - ValueListenableBuilder( - valueListenable: _isDisplayingDetail, - builder: (context, isDisplayingDetail, child) { - final mediaQuery = MediaQuery.of(context); - final bottomPadding = mediaQuery.padding.bottom; - return AnimatedPositionedDirectional( - duration: kThemeAnimationDuration, - curve: Curves.easeInOut, - bottom: isDisplayingDetail - ? 0 - : -(bottomPadding + kToolbarHeight), - start: 0, - end: 0, - height: bottomPadding + kToolbarHeight, - child: StreamGalleryFooter( - currentPage: currentPage, - totalPages: widget.mediaAttachmentPackages.length, - mediaAttachmentPackages: - widget.mediaAttachmentPackages, - mediaSelectedCallBack: (val) { - _currentPage.value = val; - _pageController.animateToPage( - val, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - Navigator.pop(context); - }, - ), - ); - }, - ), - ], - ); - }, - child: InkWell( - onTap: switchDisplayingDetail, - child: PageView.builder( - controller: _pageController, - itemCount: widget.mediaAttachmentPackages.length, - onPageChanged: (val) { - _currentPage.value = val; - if (videoPackages.isEmpty) return; - final currentAttachment = - widget.mediaAttachmentPackages[val].attachment; - for (final e in videoPackages.values) { - if (e._attachment != currentAttachment) { - e._chewieController?.pause(); - } - } - if (widget.autoplayVideos && - currentAttachment.type == 'video') { - final controller = videoPackages[currentAttachment.id]!; - controller._chewieController?.play(); - } - }, - itemBuilder: (context, index) { - final currentAttachmentPackage = - widget.mediaAttachmentPackages[index]; - final attachment = currentAttachmentPackage.attachment; - if (attachment.type == 'image' || attachment.type == 'giphy') { - final imageUrl = attachment.imageUrl ?? - attachment.assetUrl ?? - attachment.thumbUrl; - return ValueListenableBuilder( - valueListenable: _isDisplayingDetail, - builder: (context, isDisplayingDetail, _) => - AnimatedContainer( - color: isDisplayingDetail - ? StreamChannelHeaderTheme.of(context).color - : Colors.black, - duration: kThemeAnimationDuration, - child: PhotoView( - imageProvider: (imageUrl == null && - attachment.localUri != null && - attachment.file?.bytes != null) - ? Image.memory(attachment.file!.bytes!).image - : CachedNetworkImageProvider(imageUrl!), - errorBuilder: (_, __, ___) => const AttachmentError(), - loadingBuilder: (context, _) { - final image = Image.asset( - 'images/placeholder.png', - fit: BoxFit.cover, - package: 'stream_chat_flutter', - ); - final colorTheme = - StreamChatTheme.of(context).colorTheme; - return Shimmer.fromColors( - baseColor: colorTheme.disabled, - highlightColor: colorTheme.inputBg, - child: image, - ); - }, - maxScale: PhotoViewComputedScale.covered, - minScale: PhotoViewComputedScale.contained, - heroAttributes: PhotoViewHeroAttributes( - tag: widget.mediaAttachmentPackages, - ), - backgroundDecoration: const BoxDecoration( - color: Colors.transparent, - ), - ), - ), - ); - } else if (attachment.type == 'video') { - final controller = videoPackages[attachment.id]!; - if (!controller.initialized) { - return const Center( - child: CircularProgressIndicator(), - ); - } - return InkWell( - onTap: switchDisplayingDetail, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 50), - child: Chewie( - controller: controller.chewieController!, - ), - ), - ); - } - return const SizedBox(); - }, - ), - ), - ), - ); - - @override - void dispose() { - _currentPage.dispose(); - _pageController.dispose(); - _isDisplayingDetail.dispose(); - for (final package in videoPackages.values) { - package.dispose(); - } - super.dispose(); - } -} - -/// Class for packaging up things required for videos -class VideoPackage { - /// Constructor for creating [VideoPackage] - VideoPackage( - this._attachment, { - bool showControls = false, - bool autoInitialize = true, - }) : _showControls = showControls, - _autoInitialize = autoInitialize, - _videoPlayerController = _attachment.localUri != null - ? VideoPlayerController.file(File.fromUri(_attachment.localUri!)) - : VideoPlayerController.network(_attachment.assetUrl!); - - final Attachment _attachment; - final bool _showControls; - final bool _autoInitialize; - final VideoPlayerController _videoPlayerController; - ChewieController? _chewieController; - - /// Get video player for video - VideoPlayerController get videoPlayer => _videoPlayerController; - - /// Get [ChewieController] for video - ChewieController? get chewieController => _chewieController; - - /// Check if controller is initialised - bool get initialized => _videoPlayerController.value.isInitialized; - - /// Initialize all things required for [VideoPackage] - Future initialize() => _videoPlayerController.initialize().then((_) { - _chewieController = ChewieController( - videoPlayerController: _videoPlayerController, - autoInitialize: _autoInitialize, - showControls: _showControls, - aspectRatio: _videoPlayerController.value.aspectRatio, - ); - }); - - /// Add a listener to video player controller - void addListener(VoidCallback listener) => - _videoPlayerController.addListener(listener); - - /// Remove a listener to video player controller - void removeListener(VoidCallback listener) => - _videoPlayerController.removeListener(listener); - - /// Dispose controllers - Future dispose() { - _chewieController?.dispose(); - return _videoPlayerController.dispose(); - } -} diff --git a/packages/stream_chat_flutter/lib/src/fullscreen_media/fsm_enums.dart b/packages/stream_chat_flutter/lib/src/fullscreen_media/fsm_enums.dart new file mode 100644 index 00000000..5d2a1412 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/fullscreen_media/fsm_enums.dart @@ -0,0 +1,8 @@ +/// Return action for coming back from pages +enum ReturnActionType { + /// No return action + none, + + /// Go to reply message action + reply, +} diff --git a/packages/stream_chat_flutter/lib/src/fullscreen_media/fsm_stub.dart b/packages/stream_chat_flutter/lib/src/fullscreen_media/fsm_stub.dart new file mode 100644 index 00000000..1bf7c435 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/fullscreen_media/fsm_stub.dart @@ -0,0 +1,19 @@ +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/src/fullscreen_media/full_screen_media_widget.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// Stub function for returning an instance of either [FullScreenMedia] or +/// [FullScreenMediaDesktop]. +/// +/// This should ONLY be used in [FullScreenMediaBuilder]. +FullScreenMediaWidget getFsm({ + Key? key, + required List mediaAttachmentPackages, + required int startIndex, + required String userName, + ShowMessageCallback? onShowMessage, + ReplyMessageCallback? onReplyMessage, + AttachmentActionsBuilder? attachmentActionsModalBuilder, + bool? autoplayVideos, +}) => + throw UnsupportedError('Cannot create FullScreenMedia'); diff --git a/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media.dart b/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media.dart new file mode 100644 index 00000000..5aad623e --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media.dart @@ -0,0 +1,493 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:chewie/chewie.dart'; +import 'package:contextmenu/contextmenu.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:photo_view/photo_view.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:stream_chat_flutter/platform_widget_builder/platform_widget_builder.dart'; +import 'package:stream_chat_flutter/src/context_menu_items/download_menu_item.dart'; +import 'package:stream_chat_flutter/src/fullscreen_media/full_screen_media_widget.dart'; +import 'package:stream_chat_flutter/src/utils/utils.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:video_player/video_player.dart'; + +/// A full screen image widget +class StreamFullScreenMedia extends FullScreenMediaWidget { + /// Instantiate a new FullScreenImage + const StreamFullScreenMedia({ + super.key, + required this.mediaAttachmentPackages, + this.startIndex = 0, + this.userName = '', + this.onShowMessage, + this.onReplyMessage, + this.attachmentActionsModalBuilder, + this.autoplayVideos = false, + }) : assert(startIndex >= 0, 'startIndex cannot be negative'); + + /// The url of the image + final List mediaAttachmentPackages; + + /// First index of media shown + final int startIndex; + + /// Username of sender + final String userName; + + /// Callback for when show message is tapped + final ShowMessageCallback? onShowMessage; + + /// Callback for when reply message is tapped + final ReplyMessageCallback? onReplyMessage; + + /// Widget builder for attachment actions modal + /// [defaultActionsModal] is the default [AttachmentActionsModal] config + /// Use [defaultActionsModal.copyWith] to easily customize it + final AttachmentActionsBuilder? attachmentActionsModalBuilder; + + /// Auto-play videos when page is opened + final bool autoplayVideos; + + @override + _FullScreenMediaState createState() => _FullScreenMediaState(); +} + +class _FullScreenMediaState extends State { + late final PageController _pageController; + + late final _currentPage = ValueNotifier(widget.startIndex); + late final _isDisplayingDetail = ValueNotifier(true); + + void switchDisplayingDetail() { + _isDisplayingDetail.value = !_isDisplayingDetail.value; + } + + final videoPackages = {}; + + @override + void initState() { + super.initState(); + _pageController = PageController(initialPage: widget.startIndex); + for (var i = 0; i < widget.mediaAttachmentPackages.length; i++) { + final attachment = widget.mediaAttachmentPackages[i].attachment; + if (attachment.type != 'video') continue; + final package = VideoPackage(attachment, showControls: true); + videoPackages[attachment.id] = package; + } + initializePlayers(); + } + + Future initializePlayers() async { + if (videoPackages.isEmpty) { + return; + } + + final currentAttachment = + widget.mediaAttachmentPackages[widget.startIndex].attachment; + + await Future.wait(videoPackages.values.map( + (it) => it.initialize(), + )); + + if (widget.autoplayVideos && currentAttachment.type == 'video') { + final package = videoPackages.values + .firstWhere((e) => e._attachment == currentAttachment); + package._chewieController?.play(); + } + setState(() {}); // ignore: no-empty-block + } + + @override + void dispose() { + _currentPage.dispose(); + _pageController.dispose(); + _isDisplayingDetail.dispose(); + for (final package in videoPackages.values) { + package.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: false, + body: ValueListenableBuilder( + valueListenable: _currentPage, + builder: (context, currentPage, child) { + final _currentAttachmentPackage = + widget.mediaAttachmentPackages[currentPage]; + final _currentMessage = _currentAttachmentPackage.message; + final _currentAttachment = _currentAttachmentPackage.attachment; + return Stack( + children: [ + child!, + ValueListenableBuilder( + valueListenable: _isDisplayingDetail, + builder: (context, isDisplayingDetail, child) { + final mediaQuery = MediaQuery.of(context); + final topPadding = mediaQuery.padding.top; + return AnimatedPositionedDirectional( + duration: kThemeAnimationDuration, + curve: Curves.easeInOut, + top: + isDisplayingDetail ? 0 : -(topPadding + kToolbarHeight), + start: 0, + end: 0, + height: topPadding + kToolbarHeight, + child: StreamGalleryHeader( + userName: widget.userName, + sentAt: context.translations.sentAtText( + date: _currentAttachmentPackage.message.createdAt, + time: _currentAttachmentPackage.message.createdAt, + ), + onBackPressed: Navigator.of(context).pop, + message: _currentMessage, + attachment: _currentAttachment, + onShowMessage: widget.onShowMessage != null + ? () { + Navigator.pop(context); + Navigator.pop(context); + widget.onShowMessage?.call( + _currentMessage, + StreamChannel.of(context).channel, + ); + } + : null, + onReplyMessage: widget.onReplyMessage != null + ? () { + Navigator.pop(context); + Navigator.pop(context); + widget.onReplyMessage?.call( + _currentMessage, + ); + } + : null, + attachmentActionsModalBuilder: + widget.attachmentActionsModalBuilder, + ), + ); + }, + ), + if (!_currentMessage.isEphemeral) + ValueListenableBuilder( + valueListenable: _isDisplayingDetail, + builder: (context, isDisplayingDetail, child) { + final mediaQuery = MediaQuery.of(context); + final bottomPadding = mediaQuery.padding.bottom; + return AnimatedPositionedDirectional( + duration: kThemeAnimationDuration, + curve: Curves.easeInOut, + bottom: isDisplayingDetail + ? 0 + : -(bottomPadding + kToolbarHeight), + start: 0, + end: 0, + height: bottomPadding + kToolbarHeight, + child: StreamGalleryFooter( + currentPage: currentPage, + totalPages: widget.mediaAttachmentPackages.length, + mediaAttachmentPackages: widget.mediaAttachmentPackages, + mediaSelectedCallBack: (val) { + _currentPage.value = val; + _pageController.animateToPage( + val, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + Navigator.pop(context); + }, + ), + ); + }, + ), + if (widget.mediaAttachmentPackages.length > 1) ...[ + if (currentPage > 0) + GalleryNavigationItem( + left: 8, + opacityAnimation: _isDisplayingDetail, + icon: const Icon(Icons.chevron_left_rounded), + onPressed: () { + _currentPage.value--; + _pageController.previousPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }, + ), + if (currentPage < widget.mediaAttachmentPackages.length - 1) + GalleryNavigationItem( + right: 8, + opacityAnimation: _isDisplayingDetail, + icon: const Icon(Icons.chevron_right_rounded), + onPressed: () { + _currentPage.value++; + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }, + ), + ], + ], + ); + }, + child: InkWell( + onTap: switchDisplayingDetail, + child: KeyboardShortcutRunner( + onEscapeKeypress: Navigator.of(context).pop, + onLeftArrowKeypress: () { + if (_currentPage.value > 0) { + _currentPage.value--; + _pageController.previousPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + }, + onRightArrowKeypress: () { + if (_currentPage.value < + widget.mediaAttachmentPackages.length - 1) { + _currentPage.value++; + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + }, + child: PageView.builder( + controller: _pageController, + itemCount: widget.mediaAttachmentPackages.length, + onPageChanged: (val) { + _currentPage.value = val; + if (videoPackages.isEmpty) return; + final currentAttachment = + widget.mediaAttachmentPackages[val].attachment; + for (final e in videoPackages.values) { + if (e._attachment != currentAttachment) { + e._chewieController?.pause(); + } + } + if (widget.autoplayVideos && + currentAttachment.type == 'video') { + final controller = videoPackages[currentAttachment.id]!; + controller._chewieController?.play(); + } + }, + itemBuilder: (context, index) { + final currentAttachmentPackage = + widget.mediaAttachmentPackages[index]; + final attachment = currentAttachmentPackage.attachment; + if (attachment.type == 'image' || attachment.type == 'giphy') { + final imageUrl = attachment.imageUrl ?? + attachment.assetUrl ?? + attachment.thumbUrl; + return ValueListenableBuilder( + valueListenable: _isDisplayingDetail, + builder: (context, isDisplayingDetail, _) => + AnimatedContainer( + color: isDisplayingDetail + ? StreamChannelHeaderTheme.of(context).color + : Colors.black, + duration: kThemeAnimationDuration, + child: ContextMenuArea( + verticalPadding: 0, + builder: (_) => [ + DownloadMenuItem( + attachment: attachment, + ), + ], + child: PhotoView( + imageProvider: (imageUrl == null && + attachment.localUri != null && + attachment.file?.bytes != null) + ? Image.memory(attachment.file!.bytes!).image + : CachedNetworkImageProvider(imageUrl!), + errorBuilder: (_, __, ___) => const AttachmentError(), + loadingBuilder: (context, _) { + final image = Image.asset( + 'images/placeholder.png', + fit: BoxFit.cover, + package: 'stream_chat_flutter', + ); + final colorTheme = + StreamChatTheme.of(context).colorTheme; + return Shimmer.fromColors( + baseColor: colorTheme.disabled, + highlightColor: colorTheme.inputBg, + child: image, + ); + }, + maxScale: PhotoViewComputedScale.covered, + minScale: PhotoViewComputedScale.contained, + heroAttributes: PhotoViewHeroAttributes( + tag: widget.mediaAttachmentPackages, + ), + backgroundDecoration: const BoxDecoration( + color: Colors.transparent, + ), + ), + ), + ), + ); + } else if (attachment.type == 'video') { + final controller = videoPackages[attachment.id]!; + if (!controller.initialized) { + return const Center( + child: CircularProgressIndicator(), + ); + } + return InkWell( + onTap: switchDisplayingDetail, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 50), + child: ContextMenuArea( + verticalPadding: 0, + builder: (_) => [ + DownloadMenuItem( + attachment: attachment, + ), + ], + child: Chewie( + controller: controller.chewieController!, + ), + ), + ), + ); + } + return const SizedBox(); + }, + ), + ), + ), + ), + ); + } +} + +/// A widget for desktop and web users to be able to navigate left and right +/// through a gallery of images. +class GalleryNavigationItem extends StatelessWidget { + /// Builds a [GalleryNavigationItem]. + const GalleryNavigationItem({ + super.key, + required this.icon, + this.iconSize = 48, + required this.onPressed, + required this.opacityAnimation, + this.left, + this.right, + }); + + /// The icon to display. + final Widget icon; + + /// The size of the icon. + /// + /// Defaults to 48. + final double iconSize; + + /// The callback to perform when the button is clicked. + final VoidCallback onPressed; + + /// The animation for showing & hiding this widget. + final ValueListenable opacityAnimation; + + /// The left-hand placement of the button. + final double? left; + + /// The right-hand placement of the button. + final double? right; + + @override + Widget build(BuildContext context) { + return PlatformWidgetBuilder( + desktop: (_, child) => child, + web: (_, child) => child, + child: Positioned( + left: left, + right: right, + top: MediaQuery.of(context).size.height / 2, + child: ValueListenableBuilder( + valueListenable: opacityAnimation, + builder: (context, shouldShow, child) { + return AnimatedOpacity( + opacity: shouldShow ? 1 : 0, + duration: kThemeAnimationDuration, + child: child, + ); + }, + child: Material( + color: Colors.transparent, + type: MaterialType.circle, + clipBehavior: Clip.antiAlias, + child: IconButton( + icon: icon, + iconSize: iconSize, + onPressed: onPressed, + ), + ), + ), + ), + ); + } +} + +/// Class for packaging up things required for videos +class VideoPackage { + /// Constructor for creating [VideoPackage] + VideoPackage( + this._attachment, { + bool showControls = false, + bool autoInitialize = true, + }) : _showControls = showControls, + _autoInitialize = autoInitialize, + _videoPlayerController = _attachment.localUri != null + ? VideoPlayerController.file(File.fromUri(_attachment.localUri!)) + : VideoPlayerController.network(_attachment.assetUrl!); + + final Attachment _attachment; + final bool _showControls; + final bool _autoInitialize; + final VideoPlayerController _videoPlayerController; + ChewieController? _chewieController; + + /// Get video player for video + VideoPlayerController get videoPlayer => _videoPlayerController; + + /// Get [ChewieController] for video + ChewieController? get chewieController => _chewieController; + + /// Check if controller is initialised + bool get initialized => _videoPlayerController.value.isInitialized; + + /// Initialize all things required for [VideoPackage] + Future initialize() { + return _videoPlayerController.initialize().then((_) { + _chewieController = ChewieController( + videoPlayerController: _videoPlayerController, + autoInitialize: _autoInitialize, + showControls: _showControls, + aspectRatio: _videoPlayerController.value.aspectRatio, + ); + }); + } + + /// Add a listener to video player controller + void addListener(VoidCallback listener) => + _videoPlayerController.addListener(listener); + + /// Remove a listener to video player controller + void removeListener(VoidCallback listener) => + _videoPlayerController.removeListener(listener); + + /// Dispose controllers + Future dispose() { + _chewieController?.dispose(); + return _videoPlayerController.dispose(); + } +} diff --git a/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media_builder.dart b/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media_builder.dart new file mode 100644 index 00000000..f1919db1 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media_builder.dart @@ -0,0 +1,86 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/fullscreen_media/fsm_stub.dart' + if (dart.library.io) 'full_screen_media_desktop.dart' as desktop_fsm; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template fsmBuilder} +/// A wrapper widget for conditionally providing the proper +/// StreamFullScreenMedia widget when writing an application that targets +/// all available Flutter platforms (Android, iOS, macOS, Windows, Linux, +/// & Web). +/// +/// This is required because: +/// * `package:video_player` and `package:chewie` do not support macOS, Windows, +/// & Linux, but _do_ support Android, iOS, & Web. +/// * `package:dart_vlc` _does_ support macOS, Windows, & Linux via FFI. This +/// has the unfortunate consequence of not supporting Web. +/// +/// This widget makes use of dart's conditional imports to ensure that Stream's +/// desktop implementation of StreamFullScreenMedia is not imported when +/// building applications that target web. Additionally, this widget ensures +/// that applications targeting mobile platforms do not build the version of +/// StreamFullScreenMedia that targets desktop platforms (even though +/// `package:dart_vlc` technically supports iOS). +/// {@endtemplate} +class StreamFullScreenMediaBuilder extends StatelessWidget { + /// {@macro fsmBuilder} + const StreamFullScreenMediaBuilder({ + super.key, + required this.mediaAttachmentPackages, + required this.startIndex, + required this.userName, + this.onShowMessage, + this.onReplyMessage, + this.attachmentActionsModalBuilder, + this.autoplayVideos = false, + }); + + /// The url of the image + final List mediaAttachmentPackages; + + /// First index of media shown + final int startIndex; + + /// Username of sender + final String userName; + + /// Callback for when show message is tapped + final ShowMessageCallback? onShowMessage; + + /// Callback for when reply message is tapped + final ReplyMessageCallback? onReplyMessage; + + /// Widget builder for attachment actions modal + /// [defaultActionsModal] is the default [AttachmentActionsModal] config + /// Use [defaultActionsModal.copyWith] to easily customize it + final AttachmentActionsBuilder? attachmentActionsModalBuilder; + + /// Auto-play videos when page is opened + final bool autoplayVideos; + + @override + Widget build(BuildContext context) { + if (!kIsWeb && isDesktopVideoPlayerSupported) { + return desktop_fsm.getFsm( + mediaAttachmentPackages: mediaAttachmentPackages, + startIndex: startIndex, + userName: userName, + autoplayVideos: autoplayVideos, + onShowMessage: onShowMessage, + onReplyMessage: onReplyMessage, + attachmentActionsModalBuilder: attachmentActionsModalBuilder, + ); + } + + return StreamFullScreenMedia( + mediaAttachmentPackages: mediaAttachmentPackages, + startIndex: startIndex, + userName: userName, + onShowMessage: onShowMessage, + onReplyMessage: onReplyMessage, + attachmentActionsModalBuilder: attachmentActionsModalBuilder, + autoplayVideos: autoplayVideos, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media_desktop.dart b/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media_desktop.dart new file mode 100644 index 00000000..7c4a7625 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media_desktop.dart @@ -0,0 +1,520 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:contextmenu/contextmenu.dart'; +import 'package:dart_vlc/dart_vlc.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:photo_view/photo_view.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:stream_chat_flutter/platform_widget_builder/platform_widget_builder.dart'; +import 'package:stream_chat_flutter/src/context_menu_items/download_menu_item.dart'; +import 'package:stream_chat_flutter/src/fullscreen_media/full_screen_media_widget.dart'; +import 'package:stream_chat_flutter/src/utils/utils.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// Returns an instance of [FullScreenMediaDesktop]. +/// +/// This should ONLY be used in [FullScreenMediaBuilder]. +FullScreenMediaWidget getFsm({ + Key? key, + required List mediaAttachmentPackages, + required int startIndex, + required String userName, + ShowMessageCallback? onShowMessage, + ReplyMessageCallback? onReplyMessage, + AttachmentActionsBuilder? attachmentActionsModalBuilder, + bool? autoplayVideos, +}) { + return FullScreenMediaDesktop( + key: key, + mediaAttachmentPackages: mediaAttachmentPackages, + startIndex: startIndex, + userName: userName, + onReplyMessage: onReplyMessage, + onShowMessage: onShowMessage, + attachmentActionsModalBuilder: attachmentActionsModalBuilder, + autoplayVideos: autoplayVideos ?? false, + ); +} + +/// A full screen image widget +class FullScreenMediaDesktop extends FullScreenMediaWidget { + /// Instantiate a new FullScreenImage + const FullScreenMediaDesktop({ + super.key, + required this.mediaAttachmentPackages, + this.startIndex = 0, + String? userName, + this.onShowMessage, + this.onReplyMessage, + this.attachmentActionsModalBuilder, + this.autoplayVideos = false, + }) : userName = userName ?? ''; + + /// The url of the image + final List mediaAttachmentPackages; + + /// First index of media shown + final int startIndex; + + /// Username of sender + final String userName; + + /// Callback for when show message is tapped + final ShowMessageCallback? onShowMessage; + + /// Callback for when reply message is tapped + final ReplyMessageCallback? onReplyMessage; + + /// Widget builder for attachment actions modal + /// [defaultActionsModal] is the default [AttachmentActionsModal] config + /// Use [defaultActionsModal.copyWith] to easily customize it + final AttachmentActionsBuilder? attachmentActionsModalBuilder; + + /// Auto-play videos when page is opened + final bool autoplayVideos; + + @override + _FullScreenMediaDesktopState createState() => _FullScreenMediaDesktopState(); +} + +class _FullScreenMediaDesktopState extends State { + late final PageController _pageController; + + late final _currentPage = ValueNotifier(widget.startIndex); + late final _isDisplayingDetail = ValueNotifier(true); + + void switchDisplayingDetail() { + _isDisplayingDetail.value = !_isDisplayingDetail.value; + } + + final videoPackages = {}; + + @override + void initState() { + super.initState(); + _pageController = PageController(initialPage: widget.startIndex); + for (var i = 0; i < widget.mediaAttachmentPackages.length; i++) { + final attachment = widget.mediaAttachmentPackages[i].attachment; + if (attachment.type != 'video') continue; + final package = DesktopVideoPackage(attachment); + videoPackages[attachment.id] = package; + } + } + + @override + void dispose() { + _currentPage.dispose(); + _pageController.dispose(); + _isDisplayingDetail.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final containsOnlyVideos = + widget.mediaAttachmentPackages.length == videoPackages.length; + + return Scaffold( + resizeToAvoidBottomInset: false, + body: containsOnlyVideos ? _buildVideoPageView() : _buildPageView(), + ); + } + + Widget _buildVideoPageView() { + return Stack( + children: [ + ContextMenuArea( + verticalPadding: 0, + builder: (_) => [ + DownloadMenuItem( + attachment: + widget.mediaAttachmentPackages[_currentPage.value].attachment, + ), + ], + child: _PlaylistPlayer( + packages: videoPackages.values.toList(), + autoStart: widget.autoplayVideos, + ), + ), + Positioned( + left: 8, + top: 8, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + videoPackages.values.first.player.stop(); + Navigator.of(context).pop(); + }, + child: StreamSvgIcon.close( + size: 30, + ), + ), + ), + ), + ], + ); + } + + Widget _buildPageView() { + return ValueListenableBuilder( + valueListenable: _currentPage, + builder: (context, currentPage, child) { + final _currentAttachmentPackage = + widget.mediaAttachmentPackages[currentPage]; + final _currentMessage = _currentAttachmentPackage.message; + final _currentAttachment = _currentAttachmentPackage.attachment; + return Stack( + children: [ + child!, + ValueListenableBuilder( + valueListenable: _isDisplayingDetail, + builder: (context, isDisplayingDetail, child) { + final mediaQuery = MediaQuery.of(context); + final topPadding = mediaQuery.padding.top; + return AnimatedPositionedDirectional( + duration: kThemeAnimationDuration, + curve: Curves.easeInOut, + top: isDisplayingDetail ? 0 : -(topPadding + kToolbarHeight), + start: 0, + end: 0, + height: topPadding + kToolbarHeight, + child: StreamGalleryHeader( + userName: widget.userName, + sentAt: context.translations.sentAtText( + date: _currentAttachmentPackage.message.createdAt, + time: _currentAttachmentPackage.message.createdAt, + ), + onBackPressed: Navigator.of(context).pop, + message: _currentMessage, + attachment: _currentAttachment, + onShowMessage: () { + widget.onShowMessage?.call( + _currentMessage, + StreamChannel.of(context).channel, + ); + }, + attachmentActionsModalBuilder: + widget.attachmentActionsModalBuilder, + ), + ); + }, + ), + if (!_currentMessage.isEphemeral) + ValueListenableBuilder( + valueListenable: _isDisplayingDetail, + builder: (context, isDisplayingDetail, child) { + final mediaQuery = MediaQuery.of(context); + final bottomPadding = mediaQuery.padding.bottom; + return AnimatedPositionedDirectional( + duration: kThemeAnimationDuration, + curve: Curves.easeInOut, + bottom: isDisplayingDetail + ? 0 + : -(bottomPadding + kToolbarHeight), + start: 0, + end: 0, + height: bottomPadding + kToolbarHeight, + child: StreamGalleryFooter( + currentPage: currentPage, + totalPages: widget.mediaAttachmentPackages.length, + mediaAttachmentPackages: widget.mediaAttachmentPackages, + mediaSelectedCallBack: (val) { + _currentPage.value = val; + _pageController.animateToPage( + val, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + Navigator.pop(context); + }, + ), + ); + }, + ), + if (widget.mediaAttachmentPackages.length > 1) ...[ + if (currentPage > 0) + GalleryNavigationItem( + left: 8, + opacityAnimation: _isDisplayingDetail, + icon: const Icon(Icons.chevron_left_rounded), + onPressed: () { + _currentPage.value--; + _pageController.previousPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }, + ), + if (currentPage < widget.mediaAttachmentPackages.length - 1) + GalleryNavigationItem( + right: 8, + opacityAnimation: _isDisplayingDetail, + icon: const Icon(Icons.chevron_right_rounded), + onPressed: () { + _currentPage.value++; + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }, + ), + ], + ], + ); + }, + child: InkWell( + onTap: switchDisplayingDetail, + child: KeyboardShortcutRunner( + onEscapeKeypress: Navigator.of(context).pop, + onLeftArrowKeypress: () { + if (_currentPage.value > 0) { + _currentPage.value--; + _pageController.previousPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + }, + onRightArrowKeypress: () { + if (_currentPage.value < + widget.mediaAttachmentPackages.length - 1) { + _currentPage.value++; + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + }, + child: PageView.builder( + controller: _pageController, + itemCount: widget.mediaAttachmentPackages.length, + onPageChanged: (val) { + _currentPage.value = val; + if (videoPackages.isEmpty) return; + final currentAttachment = + widget.mediaAttachmentPackages[val].attachment; + for (final p in videoPackages.values) { + if (p.attachment != currentAttachment) { + p.player.pause(); + } + } + if (widget.autoplayVideos && currentAttachment.type == 'video') { + final package = videoPackages[currentAttachment.id]!; + package.player.play(); + } + }, + itemBuilder: (context, index) { + final currentAttachmentPackage = + widget.mediaAttachmentPackages[index]; + final attachment = currentAttachmentPackage.attachment; + if (attachment.type == 'image' || attachment.type == 'giphy') { + final imageUrl = attachment.imageUrl ?? + attachment.assetUrl ?? + attachment.thumbUrl; + return ValueListenableBuilder( + valueListenable: _isDisplayingDetail, + builder: (context, isDisplayingDetail, _) => + AnimatedContainer( + color: isDisplayingDetail + ? StreamChannelHeaderTheme.of(context).color + : Colors.black, + duration: kThemeAnimationDuration, + child: ContextMenuArea( + verticalPadding: 0, + builder: (_) => [ + DownloadMenuItem( + attachment: attachment, + ), + ], + child: PhotoView( + imageProvider: (imageUrl == null && + attachment.localUri != null && + attachment.file?.bytes != null) + ? Image.memory(attachment.file!.bytes!).image + : CachedNetworkImageProvider(imageUrl!), + errorBuilder: (_, __, ___) => const AttachmentError(), + loadingBuilder: (context, _) { + final image = Image.asset( + 'images/placeholder.png', + fit: BoxFit.cover, + package: 'stream_chat_flutter', + ); + final colorTheme = + StreamChatTheme.of(context).colorTheme; + return Shimmer.fromColors( + baseColor: colorTheme.disabled, + highlightColor: colorTheme.inputBg, + child: image, + ); + }, + maxScale: PhotoViewComputedScale.covered, + minScale: PhotoViewComputedScale.contained, + heroAttributes: PhotoViewHeroAttributes( + tag: widget.mediaAttachmentPackages, + ), + backgroundDecoration: const BoxDecoration( + color: Colors.transparent, + ), + ), + ), + ), + ); + } else if (attachment.type == 'video') { + final package = videoPackages[attachment.id]!; + package.player.open( + Playlist( + medias: [ + Media.network(package.attachment.assetUrl), + ], + ), + autoStart: widget.autoplayVideos, + ); + + return InkWell( + onTap: switchDisplayingDetail, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 50), + child: ContextMenuArea( + verticalPadding: 0, + builder: (_) => [ + DownloadMenuItem( + attachment: attachment, + ), + ], + child: Video( + player: package.player, + ), + ), + ), + ); + } + return const SizedBox(); + }, + ), + ), + ), + ); + } +} + +/// A widget for desktop and web users to be able to navigate left and right +/// through a gallery of images. +class GalleryNavigationItem extends StatelessWidget { + /// Builds a [GalleryNavigationItem]. + const GalleryNavigationItem({ + super.key, + required this.icon, + this.iconSize = 48, + required this.onPressed, + required this.opacityAnimation, + this.left, + this.right, + }); + + /// The icon to display. + final Widget icon; + + /// The size of the icon. + /// + /// Defaults to 48. + final double iconSize; + + /// The callback to perform when the button is clicked. + final VoidCallback onPressed; + + /// The animation for showing & hiding this widget. + final ValueListenable opacityAnimation; + + /// The left-hand placement of the button. + final double? left; + + /// The right-hand placement of the button. + final double? right; + + @override + Widget build(BuildContext context) { + return PlatformWidgetBuilder( + desktop: (_, child) => child, + web: (_, child) => child, + child: Positioned( + left: left, + right: right, + top: MediaQuery.of(context).size.height / 2, + child: ValueListenableBuilder( + valueListenable: opacityAnimation, + builder: (context, shouldShow, child) { + return AnimatedOpacity( + opacity: shouldShow ? 1 : 0, + duration: kThemeAnimationDuration, + child: child, + ); + }, + child: Material( + color: Colors.transparent, + type: MaterialType.circle, + clipBehavior: Clip.antiAlias, + child: IconButton( + icon: icon, + iconSize: iconSize, + onPressed: onPressed, + ), + ), + ), + ), + ); + } +} + +/// Class for packaging up things required for videos +class DesktopVideoPackage { + /// Constructor for creating [VideoPackage] + DesktopVideoPackage( + this.attachment, { + this.showControls = true, + }) : player = Player( + id: int.parse( + attachment.id.characters + .getRange(0, 10) + .toString() + .replaceAll(RegExp('[^0-9]'), ''), + ), + ); + + /// The video attachment to play. + final Attachment attachment; + + /// The VLC player to use. + final Player player; + + /// Whether to show the player controls or not. + final bool showControls; +} + +class _PlaylistPlayer extends StatelessWidget { + const _PlaylistPlayer({ + required this.packages, + required this.autoStart, + }); + + final List packages; + final bool autoStart; + + @override + Widget build(BuildContext context) { + final _media = []; + for (final package in packages) { + _media.add(Media.network(package.attachment.assetUrl)); + } + packages.first.player.open( + Playlist( + medias: _media, + ), + autoStart: autoStart, + ); + return Video( + player: packages.first.player, + fit: BoxFit.cover, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media_widget.dart b/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media_widget.dart new file mode 100644 index 00000000..adcac9d3 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media_widget.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +/// {@template fsmWidget} +/// An ultra-simple abstract class that allows [FullScreenMediaBuilder] +/// to call `getFsm()` and build the correct version of FullScreenMedia. +/// {@endtemplate} +abstract class FullScreenMediaWidget extends StatefulWidget { + /// {@macro fsmWidget} + const FullScreenMediaWidget({super.key}); +} diff --git a/packages/stream_chat_flutter/lib/src/gallery_footer.dart b/packages/stream_chat_flutter/lib/src/gallery/gallery_footer.dart similarity index 60% rename from packages/stream_chat_flutter/lib/src/gallery_footer.dart rename to packages/stream_chat_flutter/lib/src/gallery/gallery_footer.dart index 6a94f804..7704de7d 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_footer.dart +++ b/packages/stream_chat_flutter/lib/src/gallery/gallery_footer.dart @@ -5,20 +5,16 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/src/video/video_thumbnail_image.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@macro gallery_footer} -@Deprecated("Use 'StreamGalleryFooter' instead") -typedef GalleryFooter = StreamGalleryFooter; - -/// {@template gallery_footer} +/// {@template streamGalleryFooter} /// Footer widget for media display /// {@endtemplate} class StreamGalleryFooter extends StatefulWidget implements PreferredSizeWidget { - /// Creates a StreamGalleryFooter + /// {@macro streamGalleryFooter} const StreamGalleryFooter({ super.key, this.onBackPressed, @@ -64,6 +60,7 @@ class StreamGalleryFooter extends StatefulWidget } class _StreamGalleryFooterState extends State { + final shareButtonKey = GlobalKey(); @override Widget build(BuildContext context) { const showShareButton = !kIsWeb; @@ -84,11 +81,10 @@ class _StreamGalleryFooterState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ if (!showShareButton) - Container( - width: 48, - ) + const SizedBox(width: 48) else IconButton( + key: shareButtonKey, icon: StreamSvgIcon.iconShare( size: 24, color: galleryFooterThemeData.shareIconColor, @@ -110,8 +106,14 @@ class _StreamGalleryFooterState extends State { final filePath = '${tmpPath.path}/${attachment.id}.$type'; final file = File(filePath); await file.writeAsBytes(bytes); + final box = + shareButtonKey.currentContext?.findRenderObject(); + final position = + (box! as RenderBox).localToGlobal(Offset.zero); await Share.shareFiles( [filePath], + sharePositionOrigin: + Rect.fromLTWH(position.dx, position.dy, 0, 0), mimeTypes: [ 'image/$type', ], @@ -164,66 +166,58 @@ class _StreamGalleryFooterState extends State { ), ), builder: (context) { - const crossAxisCount = 3; - final noOfRowToShowInitially = - widget.mediaAttachmentPackages.length > crossAxisCount ? 2 : 1; - final size = MediaQuery.of(context).size; - final initialChildSize = - 48 + (size.width * noOfRowToShowInitially) / crossAxisCount; return DraggableScrollableSheet( expand: false, - initialChildSize: initialChildSize / size.height, - minChildSize: initialChildSize / size.height, - builder: (context, scrollController) => SingleChildScrollView( - controller: scrollController, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Stack( - children: [ - Center( - child: Padding( - padding: const EdgeInsets.all(16), - child: Text( - context.translations.photosLabel, - style: - galleryFooterThemeData.bottomSheetPhotosTextStyle, - ), + initialChildSize: + (CurrentPlatform.isAndroid || CurrentPlatform.isIos) ? 0.3 : 0.5, + minChildSize: 0.3, + maxChildSize: 0.7, + builder: (context, scrollController) => Column( + children: [ + Stack( + children: [ + Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + context.translations.photosLabel, + style: + galleryFooterThemeData.bottomSheetPhotosTextStyle, ), ), - Align( - alignment: Alignment.centerRight, - child: IconButton( - icon: StreamSvgIcon.close( - color: - galleryFooterThemeData.bottomSheetCloseIconColor, - ), - onPressed: () => Navigator.maybePop(context), + ), + Align( + alignment: Alignment.centerRight, + child: IconButton( + icon: StreamSvgIcon.close( + color: galleryFooterThemeData.bottomSheetCloseIconColor, ), + onPressed: () => Navigator.of(context).maybePop(), ), - ], - ), - Flexible( - child: GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: widget.mediaAttachmentPackages.length, - padding: const EdgeInsets.all(1), - // ignore: lines_longer_than_80_chars - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: crossAxisCount, - mainAxisSpacing: 2, - crossAxisSpacing: 2, - ), - itemBuilder: (context, index) { - Widget media; - final attachmentPackage = - widget.mediaAttachmentPackages[index]; - final attachment = attachmentPackage.attachment; - final message = attachmentPackage.message; - if (attachment.type == 'video') { - media = InkWell( + ), + ], + ), + Flexible( + child: GridView.builder( + shrinkWrap: true, + controller: scrollController, + itemCount: widget.mediaAttachmentPackages.length, + padding: const EdgeInsets.all(1), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + mainAxisSpacing: 2, + crossAxisSpacing: 2, + ), + itemBuilder: (context, index) { + Widget media; + final attachmentPackage = + widget.mediaAttachmentPackages[index]; + final attachment = attachmentPackage.attachment; + final message = attachmentPackage.message; + if (attachment.type == 'video') { + media = MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( onTap: () => widget.mediaSelectedCallBack!(index), child: AspectRatio( aspectRatio: 1, @@ -233,9 +227,12 @@ class _StreamGalleryFooterState extends State { fit: BoxFit.cover, ), ), - ); - } else { - media = InkWell( + ), + ); + } else { + media = MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( onTap: () => widget.mediaSelectedCallBack!(index), child: AspectRatio( aspectRatio: 1, @@ -246,45 +243,45 @@ class _StreamGalleryFooterState extends State { fit: BoxFit.cover, ), ), - ); - } + ), + ); + } - return Stack( - children: [ - media, - if (message.user != null) - Padding( - padding: const EdgeInsets.all(8), - child: Container( - padding: const EdgeInsets.all(2), - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.white.withOpacity(0.6), - boxShadow: [ - BoxShadow( - blurRadius: 8, - color: chatThemeData - .colorTheme.textHighEmphasis - .withOpacity(0.3), - ), - ], - ), - child: StreamUserAvatar( - user: message.user!, - constraints: - BoxConstraints.tight(const Size(24, 24)), - showOnlineStatus: false, - ), + return Stack( + children: [ + media, + if (message.user != null) + Padding( + padding: const EdgeInsets.all(8), + child: Container( + padding: const EdgeInsets.all(2), + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white.withOpacity(0.6), + boxShadow: [ + BoxShadow( + blurRadius: 8, + color: chatThemeData + .colorTheme.textHighEmphasis + .withOpacity(0.3), + ), + ], + ), + child: StreamUserAvatar( + user: message.user!, + constraints: + BoxConstraints.tight(const Size(24, 24)), + showOnlineStatus: false, ), ), - ], - ); - }, - ), + ), + ], + ); + }, ), - ], - ), + ), + ], ), ); }, diff --git a/packages/stream_chat_flutter/lib/src/gallery_header.dart b/packages/stream_chat_flutter/lib/src/gallery/gallery_header.dart similarity index 79% rename from packages/stream_chat_flutter/lib/src/gallery_header.dart rename to packages/stream_chat_flutter/lib/src/gallery/gallery_header.dart index 3b25f969..866e3c5e 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_header.dart +++ b/packages/stream_chat_flutter/lib/src/gallery/gallery_header.dart @@ -1,30 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:stream_chat_flutter/src/attachment_actions_modal.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; -import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/attachment_actions_modal/attachment_actions_modal.dart'; +import 'package:stream_chat_flutter/src/misc/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/theme/themes.dart'; +import 'package:stream_chat_flutter/src/utils/utils.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -/// Widget builder for attachment actions modal -/// [defaultActionsModal] is the default [AttachmentActionsModal] config -/// Use [defaultActionsModal.copyWith] to easily customize it -typedef AttachmentActionsBuilder = Widget Function( - BuildContext context, - Attachment attachment, - AttachmentActionsModal defaultActionsModal, -); - -/// {@macro gallery_header} -@Deprecated("Use 'StreamGalleryHeader' instead") -typedef GalleryHeader = StreamGalleryHeader; - -/// {@template gallery_header} +/// {@macro streamGalleryHeader} /// Header/AppBar widget for media display screen /// {@endtemplate} class StreamGalleryHeader extends StatelessWidget implements PreferredSizeWidget { - /// Creates a channel header + /// {@macro streamGalleryHeader} const StreamGalleryHeader({ super.key, required this.message, @@ -32,15 +20,19 @@ class StreamGalleryHeader extends StatelessWidget this.showBackButton = true, this.onBackPressed, this.onShowMessage, + this.onReplyMessage, this.onTitleTap, this.onImageTap, this.userName = '', this.sentAt = '', this.backgroundColor, this.attachmentActionsModalBuilder, + this.elevation = 1.0, }) : preferredSize = const Size.fromHeight(kToolbarHeight); - /// True if this header shows the leading back button + /// Whether to show the leading back button. + /// + /// Defaults to `true`. final bool showBackButton; /// Callback to call when pressing the back button. @@ -50,6 +42,9 @@ class StreamGalleryHeader extends StatelessWidget /// Callback to call when pressing the show message button. final VoidCallback? onShowMessage; + /// Callback to call when pressing the reply message button. + final VoidCallback? onReplyMessage; + /// Callback to call when the header is tapped. final VoidCallback? onTitleTap; @@ -71,11 +66,15 @@ class StreamGalleryHeader extends StatelessWidget /// The background color of this [StreamGalleryHeader]. final Color? backgroundColor; - /// Widget builder for attachment actions modal - /// [defaultActionsModal] is the default [AttachmentActionsModal] config - /// Use [defaultActionsModal.copyWith] to easily customize it + /// {@macro attachmentActionsBuilder} final AttachmentActionsBuilder? attachmentActionsModalBuilder; + /// The elevation of this [StreamGalleryHeader]. + /// + /// Defaults to `1.0`. When used for desktop & web platforms, it should + /// be set to `0.0`. + final double elevation; + @override Widget build(BuildContext context) { final galleryHeaderThemeData = StreamGalleryHeaderTheme.of(context); @@ -86,7 +85,7 @@ class StreamGalleryHeader extends StatelessWidget systemOverlayStyle: theme.brightness == Brightness.dark ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark, - elevation: 1, + elevation: elevation, leading: showBackButton ? IconButton( icon: StreamSvgIcon.close( @@ -104,9 +103,7 @@ class StreamGalleryHeader extends StatelessWidget icon: StreamSvgIcon.iconMenuPoint( color: galleryHeaderThemeData.iconMenuPointColor, ), - onPressed: () { - _showMessageActionModalBottomSheet(context); - }, + onPressed: () => _showMessageActionModalBottomSheet(context), ), ], centerTitle: true, @@ -139,7 +136,7 @@ class StreamGalleryHeader extends StatelessWidget @override final Size preferredSize; - void _showMessageActionModalBottomSheet(BuildContext context) async { + Future _showMessageActionModalBottomSheet(BuildContext context) async { final channel = StreamChannel.of(context).channel; final galleryHeaderThemeData = StreamChatTheme.of(context).galleryHeaderTheme; @@ -148,6 +145,7 @@ class StreamGalleryHeader extends StatelessWidget attachment: attachment, message: message, onShowMessage: onShowMessage, + onReply: onReplyMessage, ); final effectiveModal = attachmentActionsModalBuilder?.call( @@ -168,7 +166,7 @@ class StreamGalleryHeader extends StatelessWidget ); if (result != null) { - Navigator.pop(context, result); + Navigator.of(context).pop(result); } } } diff --git a/packages/stream_chat_flutter/lib/src/image_group.dart b/packages/stream_chat_flutter/lib/src/image_group.dart deleted file mode 100644 index 16cb7352..00000000 --- a/packages/stream_chat_flutter/lib/src/image_group.dart +++ /dev/null @@ -1,176 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@macro image_group} -@Deprecated("Use 'StreamImageGroup' instead") -typedef ImageGroup = StreamImageGroup; - -/// {@template image_group} -/// Widget for constructing a group of images in message -/// {@endtemplate} -class StreamImageGroup extends StatelessWidget { - /// Constructor for creating [StreamImageGroup] widget - const StreamImageGroup({ - super.key, - required this.images, - required this.message, - required this.messageTheme, - required this.size, - this.onReturnAction, - this.onShowMessage, - this.onAttachmentTap, - this.imageThumbnailSize = const Size(400, 400), - this.imageThumbnailResizeType = 'crop', - this.imageThumbnailCropType = 'center', - }); - - /// List of attachments to show - final List images; - - /// Callback when attachment is returned to from other screens - final ValueChanged? onReturnAction; - - /// Callback when attachment is tapped - final void Function(Message message, Attachment attachment)? onAttachmentTap; - - /// Message which images are attached to - final Message message; - - /// [StreamMessageThemeData] to apply to message - final StreamMessageThemeData messageTheme; - - /// Size of images - final Size size; - - /// Callback for when show message is tapped - final ShowMessageCallback? onShowMessage; - - /// Size of the attachment image thumbnail. - final Size imageThumbnailSize; - - /// Resize type of the image attachment thumbnail. - /// - /// Defaults to [crop] - final String /*clip|crop|scale|fill*/ imageThumbnailResizeType; - - /// Crop type of the image attachment thumbnail. - /// - /// Defaults to [center] - final String /*center|top|bottom|left|right*/ imageThumbnailCropType; - - @override - Widget build(BuildContext context) => ConstrainedBox( - constraints: BoxConstraints.loose(size), - child: Flex( - direction: Axis.vertical, - children: [ - Flexible( - fit: FlexFit.tight, - child: Flex( - crossAxisAlignment: CrossAxisAlignment.stretch, - direction: Axis.horizontal, - children: [ - Flexible( - fit: FlexFit.tight, - child: _buildImage(context, 0), - ), - Flexible( - fit: FlexFit.tight, - child: Padding( - padding: const EdgeInsets.only(left: 2), - child: _buildImage(context, 1), - ), - ), - ], - ), - ), - if (images.length >= 3) - Flexible( - fit: FlexFit.tight, - child: Padding( - padding: const EdgeInsets.only(top: 2), - child: Flex( - direction: Axis.horizontal, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Flexible( - fit: FlexFit.tight, - child: _buildImage(context, 2), - ), - if (images.length >= 4) - Flexible( - fit: FlexFit.tight, - child: Padding( - padding: const EdgeInsets.only(left: 2), - child: Stack( - fit: StackFit.expand, - children: [ - _buildImage(context, 3), - if (images.length > 4) - Positioned.fill( - child: GestureDetector( - onTap: () => _onTap(context, 3), - child: Material( - color: Colors.black38, - child: Center( - child: Text( - '+ ${images.length - 4}', - style: const TextStyle( - color: Colors.white, - fontSize: 26, - ), - ), - ), - ), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ), - ], - ), - ); - - void _onTap( - BuildContext context, - int index, - ) async { - if (onAttachmentTap != null) { - return onAttachmentTap!(message, images[index]); - } - - final channel = StreamChannel.of(context).channel; - - final res = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => StreamChannel( - channel: channel, - child: StreamFullScreenMedia( - mediaAttachmentPackages: message.getAttachmentPackageList(), - startIndex: index, - userName: message.user?.name, - onShowMessage: onShowMessage, - ), - ), - ), - ); - if (res != null) onReturnAction?.call(res); - } - - Widget _buildImage(BuildContext context, int index) => StreamImageAttachment( - attachment: images[index], - size: size, - message: message, - messageTheme: messageTheme, - onAttachmentTap: () => _onTap(context, index), - imageThumbnailSize: imageThumbnailSize, - imageThumbnailResizeType: imageThumbnailResizeType, - imageThumbnailCropType: imageThumbnailCropType, - ); -} diff --git a/packages/stream_chat_flutter/lib/src/sending_indicator.dart b/packages/stream_chat_flutter/lib/src/indicators/sending_indicator.dart similarity index 75% rename from packages/stream_chat_flutter/lib/src/sending_indicator.dart rename to packages/stream_chat_flutter/lib/src/indicators/sending_indicator.dart index 0d4d7dec..f4f1d4c8 100644 --- a/packages/stream_chat_flutter/lib/src/sending_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/indicators/sending_indicator.dart @@ -1,15 +1,11 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@macro sending_indicator} -@Deprecated("Use 'StreamSendingIndicator' instead") -typedef SendingIndicator = StreamSendingIndicator; - -/// {@template sending_indicator} -/// Used to show the sending status of the message +/// {@template streamSendingIndicator} +/// Shows the sending status of a message. /// {@endtemplate} class StreamSendingIndicator extends StatelessWidget { - /// Constructor for creating a [StreamSendingIndicator] widget + /// {@macro streamSendingIndicator} const StreamSendingIndicator({ super.key, required this.message, @@ -37,7 +33,7 @@ class StreamSendingIndicator extends StatelessWidget { if (message.status == MessageSendingStatus.sent) { return StreamSvgIcon.check( size: size, - color: IconTheme.of(context).color!.withOpacity(0.5), + color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, ); } if (message.status == MessageSendingStatus.sending || diff --git a/packages/stream_chat_flutter/lib/src/typing_indicator.dart b/packages/stream_chat_flutter/lib/src/indicators/typing_indicator.dart similarity index 82% rename from packages/stream_chat_flutter/lib/src/typing_indicator.dart rename to packages/stream_chat_flutter/lib/src/indicators/typing_indicator.dart index dfbceaff..5ad1b5ef 100644 --- a/packages/stream_chat_flutter/lib/src/typing_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/indicators/typing_indicator.dart @@ -1,17 +1,13 @@ import 'package:flutter/material.dart'; import 'package:lottie/lottie.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -/// {@macro typing_indicator} -@Deprecated("Use 'StreamTypingIndicator' instead") -typedef TypingIndicator = StreamTypingIndicator; - -/// {@template typing_indicator} -/// Widget to show the current list of typing users +/// {@template streamTypingIndicator} +/// Shows the list of user who are actively typing. /// {@endtemplate} class StreamTypingIndicator extends StatelessWidget { - /// Instantiate a new TypingIndicator + /// {@macro streamTypingIndicator} const StreamTypingIndicator({ super.key, this.channel, @@ -27,7 +23,7 @@ class StreamTypingIndicator extends StatelessWidget { /// List of typing users final Channel? channel; - /// Widget built when no typings is happening + /// The widget to build when no typing is happening final Widget? alternativeWidget; /// The padding of this widget @@ -45,7 +41,8 @@ class StreamTypingIndicator extends StatelessWidget { return BetterStreamBuilder>( initialData: channelState.typingEvents.keys, - stream: channelState.typingEventsStream.map((typings) => typings.entries + stream: channelState.typingEventsStream.map((typingEvents) => typingEvents + .entries .where((element) => element.value.parentId == parentId) .map((e) => e.key)), builder: (context, users) => AnimatedSwitcher( diff --git a/packages/stream_chat_flutter/lib/src/unread_indicator.dart b/packages/stream_chat_flutter/lib/src/indicators/unread_indicator.dart similarity index 85% rename from packages/stream_chat_flutter/lib/src/unread_indicator.dart rename to packages/stream_chat_flutter/lib/src/indicators/unread_indicator.dart index 2e5c2ed6..f13533b4 100644 --- a/packages/stream_chat_flutter/lib/src/unread_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/indicators/unread_indicator.dart @@ -1,15 +1,11 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@macro unread_indicator} -@Deprecated("Use 'StreamUnreadIndicator' instead") -typedef UnreadIndicator = StreamUnreadIndicator; - -/// {@template unread_indicator} -/// Widget for showing an unread indicator +/// {@template streamUnreadIndicator} +/// Shows an unread indicator for a message. /// {@endtemplate} class StreamUnreadIndicator extends StatelessWidget { - /// Constructor for creating an [StreamUnreadIndicator] + /// {@macro streamUnreadIndicator} const StreamUnreadIndicator({ super.key, this.cid, diff --git a/packages/stream_chat_flutter/lib/src/upload_progress_indicator.dart b/packages/stream_chat_flutter/lib/src/indicators/upload_progress_indicator.dart similarity index 86% rename from packages/stream_chat_flutter/lib/src/upload_progress_indicator.dart rename to packages/stream_chat_flutter/lib/src/indicators/upload_progress_indicator.dart index b6e2f97a..2260cf9e 100644 --- a/packages/stream_chat_flutter/lib/src/upload_progress_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/indicators/upload_progress_indicator.dart @@ -1,15 +1,11 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@macro upload_progress_indicator} -@Deprecated("Use 'StreamUploadProgressIndicator' instead") -typedef UploadProgressIndicator = StreamUploadProgressIndicator; - -/// {@template upload_progress_indicator} -/// Widget for showing upload progress +/// {@template streamUploadProgressIndicator} +/// Shows the upload progress of an attachment. /// {@endtemplate} class StreamUploadProgressIndicator extends StatelessWidget { - /// Constructor for creating an [StreamUploadProgressIndicator] + /// {@macro streamUploadProgressIndicator} const StreamUploadProgressIndicator({ super.key, required this.uploaded, diff --git a/packages/stream_chat_flutter/lib/src/keyboard_shortcuts/intents.dart b/packages/stream_chat_flutter/lib/src/keyboard_shortcuts/intents.dart new file mode 100644 index 00000000..3a5f9713 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/keyboard_shortcuts/intents.dart @@ -0,0 +1,13 @@ +import 'package:flutter/widgets.dart'; + +/// The intent for pressing the "enter" key. +class EnterKeyIntent extends Intent {} + +/// The intent for pressing the "escape" key. +class EscapeKeyIntent extends Intent {} + +/// The intent for pressing the "right" arrow key. +class RightArrowKeyIntent extends Intent {} + +/// The intent for pressing the "left" arrow key. +class LeftArrowKeyIntent extends Intent {} diff --git a/packages/stream_chat_flutter/lib/src/keyboard_shortcuts/keyboard_shortcut_runner.dart b/packages/stream_chat_flutter/lib/src/keyboard_shortcuts/keyboard_shortcut_runner.dart new file mode 100644 index 00000000..178c2690 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/keyboard_shortcuts/keyboard_shortcut_runner.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/keyboard_shortcuts/intents.dart'; +import 'package:stream_chat_flutter/src/keyboard_shortcuts/keysets.dart'; + +/// A widget that executes functions when specific physical keyboard shortcuts +/// are performed. +class KeyboardShortcutRunner extends StatelessWidget { + /// Builds a [KeyboardShortcutRunner]. + const KeyboardShortcutRunner({ + super.key, + required this.child, + this.onEnterKeypress, + this.onEscapeKeypress, + this.onRightArrowKeypress, + this.onLeftArrowKeypress, + }); + + /// This child of this widget. + final Widget child; + + /// The function to execute when the "enter" key is pressed. + final VoidCallback? onEnterKeypress; + + /// The function to execute when the "escape" key is pressed. + final VoidCallback? onEscapeKeypress; + + /// The function to execute when the "right arrow" key is pressed. + final VoidCallback? onRightArrowKeypress; + + /// The function to execute when the "left arrow" key is pressed. + final VoidCallback? onLeftArrowKeypress; + + @override + Widget build(BuildContext context) { + return FocusableActionDetector( + autofocus: true, + shortcuts: { + enterKeySet: EnterKeyIntent(), + escapeKeySet: EscapeKeyIntent(), + rightArrowKeySet: RightArrowKeyIntent(), + leftArrowKeySet: LeftArrowKeyIntent(), + }, + actions: { + EnterKeyIntent: CallbackAction( + onInvoke: (e) => onEnterKeypress?.call(), + ), + EscapeKeyIntent: CallbackAction( + onInvoke: (e) => onEscapeKeypress?.call(), + ), + RightArrowKeyIntent: CallbackAction( + onInvoke: (e) => onRightArrowKeypress?.call(), + ), + LeftArrowKeyIntent: CallbackAction( + onInvoke: (e) => onLeftArrowKeypress?.call(), + ), + }, + child: child, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/keyboard_shortcuts/keysets.dart b/packages/stream_chat_flutter/lib/src/keyboard_shortcuts/keysets.dart new file mode 100644 index 00000000..495be7d9 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/keyboard_shortcuts/keysets.dart @@ -0,0 +1,32 @@ +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +/// The "enter" keyset. +/// +/// Use to quickly send a message in [StreamMessageInput]. +final enterKeySet = LogicalKeySet( + LogicalKeyboardKey.enter, +); + +/// The "escape" keyset. +/// +/// Use for: +/// * Removing a reply from [StreamMessageInput]. +/// * Closing [FullScreenMediaDesktop]. +final escapeKeySet = LogicalKeySet( + LogicalKeyboardKey.escape, +); + +/// The "right arrow" keyset. +/// +/// Use for navigating to the next [FullScreenMediaDesktop] item. +final rightArrowKeySet = LogicalKeySet( + LogicalKeyboardKey.arrowRight, +); + +/// The "left arrow" keyset. +/// +/// Use for navigating to the previous [FullScreenMediaDesktop] item. +final leftArrowKeySet = LogicalKeySet( + LogicalKeyboardKey.arrowLeft, +); diff --git a/packages/stream_chat_flutter/lib/src/localization/stream_chat_localizations.dart b/packages/stream_chat_flutter/lib/src/localization/stream_chat_localizations.dart index e9f8cf4b..66c09e88 100644 --- a/packages/stream_chat_flutter/lib/src/localization/stream_chat_localizations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/stream_chat_localizations.dart @@ -1,5 +1,4 @@ import 'package:flutter/widgets.dart'; - import 'package:stream_chat_flutter/src/localization/translations.dart' show Translations; @@ -28,9 +27,10 @@ abstract class StreamChatLocalizations implements Translations { /// ```dart /// tooltip: StreamChatLocalizations.of(context).streamChatLabel, /// ``` - static StreamChatLocalizations? of(BuildContext context) => - Localizations.of( - context, - StreamChatLocalizations, - ); + static StreamChatLocalizations? of(BuildContext context) { + return Localizations.of( + context, + StreamChatLocalizations, + ); + } } diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index 27094c80..d41dfcb8 100644 --- a/packages/stream_chat_flutter/lib/src/localization/translations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -1,7 +1,6 @@ import 'package:jiffy/jiffy.dart'; -import 'package:stream_chat_flutter/src/connection_status_builder.dart'; -import 'package:stream_chat_flutter/src/message_list_view.dart'; -import 'package:stream_chat_flutter/src/v4/message_input/stream_message_input.dart'; +import 'package:stream_chat_flutter/src/message_list_view/message_list_view.dart'; +import 'package:stream_chat_flutter/src/misc/connection_status_builder.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart' show User; @@ -113,7 +112,7 @@ abstract class Translations { /// The label for instant commands in [StreamMessageInput] String get instantCommandsLabel; - /// The error shown in case the fi"le is too large even after compression + /// The error shown in case the file is too large even after compression /// while uploading via [StreamMessageInput] String fileTooLargeAfterCompressionError(double limitInMB); @@ -121,8 +120,8 @@ abstract class Translations { /// while uploading via [StreamMessageInput] String fileTooLargeError(double limitInMB); - /// The text for showing the query while searching for emojis - String emojiMatchingQueryText(String query); + /// The error shown when the file being read has no bytes + String get couldNotReadBytesFromFileError; /// The label for "add a file" String get addAFileLabel; @@ -160,9 +159,15 @@ abstract class Translations { /// The message shown for asking photo and video access permission String get enablePhotoAndVideoAccessMessage; + /// The message shown for asking photo and video access permission + String get enableFileAccessMessage; + /// The message shown for asking gallery access permission String get allowGalleryAccessMessage; + /// The message shown for asking file access permission + String get allowFileAccessMessage; + /// The label for "flag message" String get flagMessageLabel; @@ -330,6 +335,26 @@ abstract class Translations { /// Label for "Attachment limit exceeded: /// it's not possible to add more than $limit attachments" String attachmentLimitExceedError(int limit); + + /// The label for "Download" + String get downloadLabel; + + /// The text for "Mute Group"/"Unmute Group" based on the value of [isMuted]. + String toggleMuteUnmuteGroupText({required bool isMuted}); + + /// The text for "Mute User"/"Unmute User" based on the value of [isMuted]. + String toggleMuteUnmuteUserText({required bool isMuted}); + + /// The text for "Are you sure you want to mute this group?"/"Are you sure you want to unmute this group?" + /// based on the value of [isMuted]. + String toggleMuteUnmuteGroupQuestion({required bool isMuted}); + + /// The text for "Are you sure you want to mute this user?"/"Are you sure you want to unmute this user?" + /// based on the value of [isMuted]. + String toggleMuteUnmuteUserQuestion({required bool isMuted}); + + /// The text for "MUTE"/"UNMUTE" based on the value of [isMuted]. + String toggleMuteUnmuteAction({required bool isMuted}); } /// Default implementation of Translation strings for the stream chat widgets @@ -462,7 +487,8 @@ class DefaultTranslations implements Translations { 'The file is too large to upload. The file size limit is $limitInMB MB.'; @override - String emojiMatchingQueryText(String query) => 'Emoji matching "$query"'; + String get couldNotReadBytesFromFileError => + 'Could not read bytes from file.'; @override String get addAFileLabel => 'Add a file'; @@ -709,6 +735,54 @@ class DefaultTranslations implements Translations { String attachmentLimitExceedError(int limit) => """ Attachment limit exceeded: it's not possible to add more than $limit attachments"""; + @override + String get downloadLabel => 'Download'; + + @override + String toggleMuteUnmuteUserText({required bool isMuted}) { + if (isMuted) { + return 'Unmute User'; + } else { + return 'Mute User'; + } + } + + @override + String toggleMuteUnmuteGroupQuestion({required bool isMuted}) { + if (isMuted) { + return 'Are you sure you want to unmute this group?'; + } else { + return 'Are you sure you want to mute this group?'; + } + } + + @override + String toggleMuteUnmuteUserQuestion({required bool isMuted}) { + if (isMuted) { + return 'Are you sure you want to unmute this user?'; + } else { + return 'Are you sure you want to mute this user?'; + } + } + + @override + String toggleMuteUnmuteAction({required bool isMuted}) { + if (isMuted) { + return 'UNMUTE'; + } else { + return 'MUTE'; + } + } + + @override + String toggleMuteUnmuteGroupText({required bool isMuted}) { + if (isMuted) { + return 'Unmute Group'; + } else { + return 'Mute Group'; + } + } + @override String get linkDisabledDetails => 'Sending links is not allowed in this conversation.'; @@ -723,4 +797,11 @@ Attachment limit exceeded: it's not possible to add more than $limit attachments } return '$unreadCount unread messages'; } + + @override + String get enableFileAccessMessage => 'Please enable access to files' + '\nso you can share them with friends.'; + + @override + String get allowFileAccessMessage => 'Allow access to files'; } diff --git a/packages/stream_chat_flutter/lib/src/media_list_view.dart b/packages/stream_chat_flutter/lib/src/media_list_view.dart deleted file mode 100644 index 7dce7864..00000000 --- a/packages/stream_chat_flutter/lib/src/media_list_view.dart +++ /dev/null @@ -1,320 +0,0 @@ -import 'dart:ui' as ui; - -import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:photo_manager/photo_manager.dart'; -import 'package:stream_chat_flutter/src/media_list_view_controller.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@macro media_list_view} -@Deprecated("Use 'StreamMediaListView' instead") -typedef MediaListView = StreamMediaListView; - -/// {@template media_list_view} -/// Constructs a list of media -/// {@endtemplate} -class StreamMediaListView extends StatefulWidget { - /// Constructor for creating a [StreamMediaListView] widget - const StreamMediaListView({ - super.key, - this.selectedIds = const [], - this.onSelect, - this.controller, - this.thumbnailSize = const ThumbnailSize(400, 400), - this.thumbnailFormat = ThumbnailFormat.jpeg, - this.thumbnailQuality = 100, - this.thumbnailScale = 1, - }); - - /// Stores the media selected - final List selectedIds; - - /// Callback for on media selected - final void Function(AssetEntity media)? onSelect; - - /// Controller that handles MediaListView - final MediaListViewController? controller; - - /// The thumbnail size. - final ThumbnailSize thumbnailSize; - - /// {@macro photo_manager.ThumbnailFormat} - final ThumbnailFormat thumbnailFormat; - - /// The quality value for the thumbnail. - /// - /// Valid from 1 to 100. - /// Defaults to 100. - final int thumbnailQuality; - - /// Scale of the image. - final double thumbnailScale; - - @override - _StreamMediaListViewState createState() => _StreamMediaListViewState(); -} - -class _StreamMediaListViewState extends State { - var _media = []; - var _currentPage = 0; - final _scrollController = ScrollController(); - - /// Controller necessary to verify limited access to photo gallery in iOS and - /// update the media list when listerners are emitted - late final controller = widget.controller ?? MediaListViewController(); - - @override - Widget build(BuildContext context) => LazyLoadScrollView( - onEndOfPage: () async { - await _getMedia(); - _updatePage(); - }, - child: GridView.builder( - itemCount: _media.length, - controller: _scrollController, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - ), - itemBuilder: ( - context, - position, - ) { - final media = _media.elementAt(position); - final chatThemeData = StreamChatTheme.of(context); - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 1, vertical: 1), - child: InkWell( - onTap: widget.onSelect == null - ? null - : () => widget.onSelect!(media), - child: Stack( - children: [ - AspectRatio( - aspectRatio: 1, - child: FadeInImage( - image: MediaThumbnailProvider( - media: media, - size: widget.thumbnailSize, - format: widget.thumbnailFormat, - quality: widget.thumbnailQuality, - scale: widget.thumbnailScale, - ), - fadeInDuration: const Duration(milliseconds: 300), - placeholder: const AssetImage( - 'images/placeholder.png', - package: 'stream_chat_flutter', - ), - fit: BoxFit.cover, - ), - ), - Positioned.fill( - child: IgnorePointer( - child: AnimatedOpacity( - duration: const Duration(milliseconds: 300), - opacity: - widget.selectedIds.any((id) => id == media.id) - ? 1.0 - : 0.0, - child: Container( - color: chatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.5), - alignment: Alignment.topRight, - padding: const EdgeInsets.only( - top: 8, - right: 8, - ), - child: CircleAvatar( - radius: 12, - backgroundColor: chatThemeData.colorTheme.barsBg, - child: StreamSvgIcon.check( - size: 24, - color: - chatThemeData.colorTheme.textHighEmphasis, - ), - ), - ), - ), - ), - ), - if (media.type == AssetType.video) ...[ - Positioned( - left: 8, - bottom: 10, - child: SvgPicture.asset( - 'svgs/video_call_icon.svg', - package: 'stream_chat_flutter', - ), - ), - Positioned( - right: 4, - bottom: 10, - child: Text( - media.videoDuration.format(), - style: TextStyle( - color: chatThemeData.colorTheme.barsBg, - ), - ), - ), - ], - ], - ), - ), - ); - }, - ), - ); - - @override - void initState() { - super.initState(); - controller.addListener(_updateMediaList); - _getMedia(); - } - - @override - void dispose() { - super.dispose(); - controller.removeListener(_updateMediaList); - if (widget.controller == null) { - controller.dispose(); - } - } - - void _updateMediaList() { - if (controller.shouldUpdateMedia) { - _getMedia(); - } - } - - void _updatePage() { - ++_currentPage; - } - - Future _getMedia() async { - final assetList = (await PhotoManager.getAssetPathList( - filterOption: FilterOptionGroup( - orders: [ - const OrderOption( - // ignore: avoid_redundant_argument_values - type: OrderOptionType.createDate, - ), - ], - ), - onlyAll: true, - )) - .firstOrNull; - - final media = await assetList?.getAssetListPaged( - page: _currentPage, - size: 50, - ); - if (media?.isNotEmpty == true) { - setState(() { - _media = media!; - }); - } - } -} - -/// ImageProvider implementation for [AssetEntity]. -class MediaThumbnailProvider extends ImageProvider { - /// Constructor for creating a [MediaThumbnailProvider] - const MediaThumbnailProvider({ - required this.media, - // TODO: Are these sizes optimal? Consider web/desktop - this.size = const ThumbnailSize(400, 400), - this.format = ThumbnailFormat.jpeg, - this.quality = 100, - this.scale = 2, - }); - - /// Media to get the thumbnail from. - final AssetEntity media; - - /// The thumbnail size. - final ThumbnailSize size; - - /// {@macro photo_manager.ThumbnailFormat} - final ThumbnailFormat format; - - /// The quality value for the thumbnail. - /// - /// Valid from 1 to 100. - /// Defaults to 100. - final int quality; - - /// Scale of the image. - final double scale; - - @override - Future obtainKey(ImageConfiguration configuration) { - return SynchronousFuture(this); - } - - @override - ImageStreamCompleter load( - MediaThumbnailProvider key, - DecoderCallback decode, - ) { - return MultiFrameImageStreamCompleter( - codec: _loadAsync(key, decode), - scale: key.scale, - informationCollector: () sync* { - yield DiagnosticsProperty( - 'Thumbnail provider: $this \n Thumbnail key: $key', - this, - style: DiagnosticsTreeStyle.errorProperty, - ); - }, - ); - } - - Future _loadAsync( - MediaThumbnailProvider key, - DecoderCallback decode, - ) async { - assert(key == this, '$key is not $this'); - final bytes = await media.thumbnailDataWithSize( - size, - format: format, - quality: quality, - ); - return decode(bytes!); - } - - @override - bool operator ==(dynamic other) { - if (other is MediaThumbnailProvider) { - return media == other.media && - size == other.size && - format == other.format && - quality == other.quality && - scale == other.scale; - } - return false; - } - - @override - int get hashCode => hashValues(media, size, format, quality, scale); - - @override - String toString() => '$runtimeType(' - 'media: $media, ' - 'size: $size, ' - 'format: $format, ' - 'quality: $quality, ' - 'scale: $scale' - ')'; -} - -extension on Duration { - String format() { - final s = '$this'.split('.')[0].padLeft(8, '0'); - if (s.startsWith('00:')) { - return s.replaceFirst('00:', ''); - } - return s; - } -} diff --git a/packages/stream_chat_flutter/lib/src/media_list_view_controller.dart b/packages/stream_chat_flutter/lib/src/media_list_view_controller.dart deleted file mode 100644 index 9ec3f594..00000000 --- a/packages/stream_chat_flutter/lib/src/media_list_view_controller.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:flutter/material.dart'; - -/// Controller for MediaListView Widget -class MediaListViewController extends ChangeNotifier { - var _shouldUpdateMedia = false; - - /// Getter that knows if the media should be updated. - bool get shouldUpdateMedia => _shouldUpdateMedia; - - /// Method that update shouldUpdateMedia and notify all listeners - /// about this update. - void updateMedia({required bool newValue}) { - _shouldUpdateMedia = newValue; - notifyListeners(); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart deleted file mode 100644 index ec9b8a2e..00000000 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ /dev/null @@ -1,689 +0,0 @@ -import 'dart:ui'; -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@macro message_actions_modal} -@Deprecated("Use 'StreamMessageActionsModal' instead") -typedef MessageActionsModal = StreamMessageActionsModal; - -/// {@template message_actions_modal} -/// Constructs a modal with actions for a message -/// {@endtemplate} -class StreamMessageActionsModal extends StatefulWidget { - /// Constructor for creating a [StreamMessageActionsModal] widget - const StreamMessageActionsModal({ - super.key, - required this.message, - required this.messageWidget, - required this.messageTheme, - this.showReactions, - this.showDeleteMessage, - this.showEditMessage, - this.onReplyTap, - this.onThreadReplyTap, - this.showCopyMessage = true, - this.showReplyMessage = true, - this.showResendMessage = true, - this.showThreadReplyMessage, - this.showFlagButton, - this.showPinButton, - this.editMessageInputBuilder, - this.reverse = false, - this.customActions = const [], - this.onCopyTap, - }); - - /// Widget that shows the message - final Widget messageWidget; - - /// Builder for edit message - final Widget Function(BuildContext, Message)? editMessageInputBuilder; - - /// Callback for when thread reply is tapped - final OnMessageTap? onThreadReplyTap; - - /// Callback for when reply is tapped - final OnMessageTap? onReplyTap; - - /// Message in focus for actions - final Message message; - - /// [StreamMessageThemeData] for message - final StreamMessageThemeData messageTheme; - - /// Flag for showing reactions - final bool? showReactions; - - /// Callback when copy is tapped - final OnMessageTap? onCopyTap; - - /// Callback when delete is tapped - final bool? showDeleteMessage; - - /// Flag for showing copy action - final bool showCopyMessage; - - /// Flag for showing edit action - final bool? showEditMessage; - - /// Flag for showing resend action - final bool showResendMessage; - - /// Flag for showing reply action - final bool? showReplyMessage; - - /// Flag for showing thread reply action - final bool? showThreadReplyMessage; - - /// Flag for showing flag action - final bool? showFlagButton; - - /// Flag for showing pin action - final bool? showPinButton; - - /// Flag for reversing message - final bool reverse; - - /// List of custom actions - final List customActions; - - @override - _StreamMessageActionsModalState createState() => - _StreamMessageActionsModalState(); -} - -class _StreamMessageActionsModalState extends State { - bool _showActions = true; - late List _userPermissions; - late bool _isMyMessage; - - @override - Widget build(BuildContext context) => _showMessageOptionsModal(); - - Widget _showMessageOptionsModal() { - final mediaQueryData = MediaQuery.of(context); - final size = mediaQueryData.size; - final user = StreamChat.of(context).currentUser; - - final roughMaxSize = size.width * 2 / 3; - var messageTextLength = widget.message.text!.length; - if (widget.message.quotedMessage != null) { - var quotedMessageLength = - (widget.message.quotedMessage!.text?.length ?? 0) + 40; - if (widget.message.quotedMessage!.attachments.isNotEmpty) { - quotedMessageLength += 40; - } - if (quotedMessageLength > messageTextLength) { - messageTextLength = quotedMessageLength; - } - } - final roughSentenceSize = messageTextLength * - (widget.messageTheme.messageTextStyle?.fontSize ?? 1) * - 1.2; - final divFactor = widget.message.attachments.isNotEmpty - ? 1 - : (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize)); - - final streamChatThemeData = StreamChatTheme.of(context); - - final numberOfReactions = streamChatThemeData.reactionIcons.length; - final shiftFactor = - numberOfReactions < 5 ? (5 - numberOfReactions) * 0.1 : 0.0; - - final hasEditPermission = _userPermissions.contains( - PermissionType.updateAnyMessage, - ) || - _userPermissions.contains(PermissionType.updateOwnMessage); - - final hasDeletePermission = _userPermissions.contains( - PermissionType.deleteAnyMessage, - ) || - _userPermissions.contains(PermissionType.deleteOwnMessage); - - final hasReactionPermission = - _userPermissions.contains(PermissionType.sendReaction); - - final child = Center( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(8), - child: Column( - crossAxisAlignment: widget.reverse - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - if ((widget.showReactions ?? hasReactionPermission) && - (widget.message.status == MessageSendingStatus.sent)) - Align( - alignment: Alignment( - user?.id == widget.message.user?.id - ? (divFactor >= 1.0 - ? -0.2 - shiftFactor - : (1.2 - divFactor)) - : (divFactor >= 1.0 - ? shiftFactor + 0.2 - : -(1.2 - divFactor)), - 0, - ), - child: StreamReactionPicker( - message: widget.message, - ), - ), - const SizedBox(height: 8), - IgnorePointer( - child: widget.messageWidget, - ), - const SizedBox(height: 8), - Padding( - padding: EdgeInsets.only( - left: widget.reverse ? 0 : 40, - ), - child: SizedBox( - width: mediaQueryData.size.width * 0.75, - child: Material( - color: streamChatThemeData.colorTheme.appBg, - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (widget.showReplyMessage ?? - (_userPermissions - .contains(PermissionType.quoteMessage) && - widget.message.status == - MessageSendingStatus.sent)) - _buildReplyButton(context), - if (widget.showThreadReplyMessage ?? - _userPermissions - .contains(PermissionType.sendReply) && - (widget.message.status == - MessageSendingStatus.sent) && - widget.message.parentId == null) - _buildThreadReplyButton(context), - if (widget.showResendMessage) - _buildResendMessage(context), - if (widget.showEditMessage ?? - _isMyMessage && hasEditPermission) - _buildEditMessage(context), - if (widget.showCopyMessage) _buildCopyButton(context), - if (widget.showFlagButton ?? - _userPermissions - .contains(PermissionType.flagMessage)) - _buildFlagButton(context), - if (widget.showPinButton ?? - _userPermissions - .contains(PermissionType.pinMessage)) - _buildPinButton(context), - if (widget.showDeleteMessage ?? - (_isMyMessage && hasDeletePermission)) - _buildDeleteButton(context), - ...widget.customActions - .map((action) => _buildCustomAction( - context, - action, - )), - ].insertBetween( - Container( - height: 1, - color: streamChatThemeData.colorTheme.borders, - ), - ), - ), - ), - ), - ), - ], - ), - ), - ), - ); - - return GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () => Navigator.maybePop(context), - child: Stack( - children: [ - Positioned.fill( - child: BackdropFilter( - filter: ImageFilter.blur( - sigmaX: 10, - sigmaY: 10, - ), - child: ColoredBox( - color: streamChatThemeData.colorTheme.overlay, - ), - ), - ), - if (_showActions) - TweenAnimationBuilder( - tween: Tween(begin: 0, end: 1), - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOutBack, - builder: (context, val, child) => Transform.scale( - scale: val, - child: child, - ), - child: child, - ), - ], - ), - ); - } - - InkWell _buildCustomAction( - BuildContext context, - StreamMessageAction messageAction, - ) => - InkWell( - onTap: () { - messageAction.onTap?.call(widget.message); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), - child: Row( - children: [ - messageAction.leading ?? const Offstage(), - const SizedBox(width: 16), - messageAction.title ?? const Offstage(), - ], - ), - ), - ); - - void _showFlagDialog() async { - final client = StreamChat.of(context).client; - - final streamChatThemeData = StreamChatTheme.of(context); - final answer = await showConfirmationDialog( - context, - title: context.translations.flagMessageLabel, - icon: StreamSvgIcon.flag( - color: streamChatThemeData.colorTheme.accentError, - size: 24, - ), - question: context.translations.flagMessageQuestion, - okText: context.translations.flagLabel, - cancelText: context.translations.cancelLabel, - ); - - final theme = streamChatThemeData; - if (answer == true) { - try { - await client.flagMessage(widget.message.id); - await showInfoDialog( - context, - icon: StreamSvgIcon.flag( - color: theme.colorTheme.accentError, - size: 24, - ), - details: context.translations.flagMessageSuccessfulText, - title: context.translations.flagMessageSuccessfulLabel, - okText: context.translations.okLabel, - ); - } catch (err) { - if (err is StreamChatNetworkError && - err.errorCode == ChatErrorCode.inputError) { - await showInfoDialog( - context, - icon: StreamSvgIcon.flag( - color: theme.colorTheme.accentError, - size: 24, - ), - details: context.translations.flagMessageSuccessfulText, - title: context.translations.flagMessageSuccessfulLabel, - okText: context.translations.okLabel, - ); - } else { - _showErrorAlert(); - } - } - } - } - - void _togglePin() async { - final channel = StreamChannel.of(context).channel; - - Navigator.pop(context); - try { - if (!widget.message.pinned) { - await channel.pinMessage(widget.message); - } else { - await channel.unpinMessage(widget.message); - } - } catch (e) { - _showErrorAlert(); - } - } - - void _showDeleteDialog() async { - setState(() { - _showActions = false; - }); - final answer = await showConfirmationDialog( - context, - title: context.translations.deleteMessageLabel, - icon: StreamSvgIcon.flag( - color: StreamChatTheme.of(context).colorTheme.accentError, - size: 24, - ), - question: context.translations.deleteMessageQuestion, - okText: context.translations.deleteLabel, - cancelText: context.translations.cancelLabel, - ); - - if (answer == true) { - try { - Navigator.pop(context); - await StreamChannel.of(context).channel.deleteMessage(widget.message); - } catch (err) { - _showErrorAlert(); - } - } else { - setState(() { - _showActions = true; - }); - } - } - - void _showErrorAlert() { - showInfoDialog( - context, - icon: StreamSvgIcon.error( - color: StreamChatTheme.of(context).colorTheme.accentError, - size: 24, - ), - details: context.translations.operationCouldNotBeCompletedText, - title: context.translations.somethingWentWrongError, - okText: context.translations.okLabel, - ); - } - - Widget _buildReplyButton(BuildContext context) { - final streamChatThemeData = StreamChatTheme.of(context); - return InkWell( - onTap: () { - Navigator.pop(context); - widget.onReplyTap?.call(widget.message); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), - child: Row( - children: [ - StreamSvgIcon.reply( - color: streamChatThemeData.primaryIconTheme.color, - ), - const SizedBox(width: 16), - Text( - context.translations.replyLabel, - style: streamChatThemeData.textTheme.body, - ), - ], - ), - ), - ); - } - - Widget _buildFlagButton(BuildContext context) { - final streamChatThemeData = StreamChatTheme.of(context); - return InkWell( - onTap: _showFlagDialog, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), - child: Row( - children: [ - StreamSvgIcon.iconFlag( - color: streamChatThemeData.primaryIconTheme.color, - ), - const SizedBox(width: 16), - Text( - context.translations.flagMessageLabel, - style: streamChatThemeData.textTheme.body, - ), - ], - ), - ), - ); - } - - Widget _buildPinButton(BuildContext context) { - final streamChatThemeData = StreamChatTheme.of(context); - return InkWell( - onTap: _togglePin, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), - child: Row( - children: [ - StreamSvgIcon.pin( - color: streamChatThemeData.primaryIconTheme.color, - size: 24, - ), - const SizedBox(width: 16), - Text( - context.translations.togglePinUnpinText( - pinned: widget.message.pinned, - ), - style: streamChatThemeData.textTheme.body, - ), - ], - ), - ), - ); - } - - Widget _buildDeleteButton(BuildContext context) { - final isDeleteFailed = - widget.message.status == MessageSendingStatus.failed_delete; - return InkWell( - onTap: _showDeleteDialog, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), - child: Row( - children: [ - StreamSvgIcon.delete( - color: Colors.red, - ), - const SizedBox(width: 16), - Text( - context.translations.toggleDeleteRetryDeleteMessageText( - isDeleteFailed: isDeleteFailed, - ), - style: StreamChatTheme.of(context) - .textTheme - .body - .copyWith(color: Colors.red), - ), - ], - ), - ), - ); - } - - Widget _buildCopyButton(BuildContext context) { - final streamChatThemeData = StreamChatTheme.of(context); - return InkWell( - onTap: () async { - widget.onCopyTap?.call(widget.message); - Navigator.pop(context); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), - child: Row( - children: [ - StreamSvgIcon.copy( - size: 24, - color: streamChatThemeData.primaryIconTheme.color, - ), - const SizedBox(width: 16), - Text( - context.translations.copyMessageLabel, - style: streamChatThemeData.textTheme.body, - ), - ], - ), - ), - ); - } - - Widget _buildEditMessage(BuildContext context) { - final streamChatThemeData = StreamChatTheme.of(context); - return InkWell( - onTap: () async { - Navigator.pop(context); - _showEditBottomSheet(context); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), - child: Row( - children: [ - StreamSvgIcon.edit( - color: streamChatThemeData.primaryIconTheme.color, - ), - const SizedBox(width: 16), - Text( - context.translations.editMessageLabel, - style: streamChatThemeData.textTheme.body, - ), - ], - ), - ), - ); - } - - Widget _buildResendMessage(BuildContext context) { - final isUpdateFailed = - widget.message.status == MessageSendingStatus.failed_update; - final streamChatThemeData = StreamChatTheme.of(context); - return InkWell( - onTap: () { - Navigator.pop(context); - final channel = StreamChannel.of(context).channel; - if (isUpdateFailed) { - channel.updateMessage(widget.message); - } else { - channel.sendMessage(widget.message); - } - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), - child: Row( - children: [ - StreamSvgIcon.circleUp( - color: streamChatThemeData.colorTheme.accentPrimary, - ), - const SizedBox(width: 16), - Text( - context.translations.toggleResendOrResendEditedMessage( - isUpdateFailed: isUpdateFailed, - ), - style: streamChatThemeData.textTheme.body, - ), - ], - ), - ), - ); - } - - void _showEditBottomSheet(BuildContext context) { - final channel = StreamChannel.of(context).channel; - final streamChatThemeData = StreamChatTheme.of(context); - showModalBottomSheet( - context: context, - elevation: 2, - clipBehavior: Clip.hardEdge, - isScrollControlled: true, - backgroundColor: StreamMessageInputTheme.of(context).inputBackgroundColor, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - ), - builder: (context) => Padding( - padding: MediaQuery.of(context).viewInsets, - child: StreamChannel( - channel: channel, - child: Flex( - direction: Axis.vertical, - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.all(8), - child: StreamSvgIcon.edit( - color: streamChatThemeData.colorTheme.disabled, - ), - ), - Text( - context.translations.editMessageLabel, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - IconButton( - visualDensity: VisualDensity.compact, - icon: StreamSvgIcon.closeSmall(), - onPressed: Navigator.of(context).pop, - ), - ], - ), - ), - if (widget.editMessageInputBuilder != null) - widget.editMessageInputBuilder!(context, widget.message) - else - StreamMessageInput( - messageInputController: StreamMessageInputController( - message: widget.message, - ), - preMessageSending: (m) { - FocusScope.of(context).unfocus(); - Navigator.pop(context); - return m; - }, - ), - ], - ), - ), - ), - ); - } - - Widget _buildThreadReplyButton(BuildContext context) { - final streamChatThemeData = StreamChatTheme.of(context); - return InkWell( - onTap: () { - Navigator.pop(context); - widget.onThreadReplyTap?.call(widget.message); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), - child: Row( - children: [ - StreamSvgIcon.thread( - color: streamChatThemeData.primaryIconTheme.color, - ), - const SizedBox(width: 16), - Text( - context.translations.threadReplyLabel, - style: streamChatThemeData.textTheme.body, - ), - ], - ), - ), - ); - } - - @override - void didChangeDependencies() { - final newStreamChannel = StreamChannel.of(context); - _userPermissions = newStreamChannel.channel.ownCapabilities; - _isMyMessage = - widget.message.user?.id == StreamChat.of(context).currentUser?.id; - super.didChangeDependencies(); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/copy_message_button.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/copy_message_button.dart new file mode 100644 index 00000000..ed9fe4f6 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal/copy_message_button.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template copyMessageButton} +/// Allows a user to copy the text of a message. +/// +/// Used by [MessageActionsModal]. Should not be used by itself. +/// {@endtemplate} +class CopyMessageButton extends StatelessWidget { + /// {@macro copyMessageButton} + const CopyMessageButton({ + super.key, + required this.onTap, + }); + + /// The callback to perform when the button is tapped. + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final streamChatThemeData = StreamChatTheme.of(context); + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), + child: Row( + children: [ + StreamSvgIcon.copy( + size: 24, + color: streamChatThemeData.primaryIconTheme.color, + ), + const SizedBox(width: 16), + Text( + context.translations.copyMessageLabel, + style: streamChatThemeData.textTheme.body, + ), + ], + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/delete_message_button.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/delete_message_button.dart new file mode 100644 index 00000000..e97afec0 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal/delete_message_button.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template deleteMessageButton} +/// A button that allows a user to delete the selected message. +/// +/// Used by [MessageActionsModal]. Should not be used by itself. +/// {@endtemplate} +class DeleteMessageButton extends StatelessWidget { + /// {@macro deleteMessageButton} + const DeleteMessageButton({ + super.key, + required this.isDeleteFailed, + required this.onTap, + }); + + /// Indicates whether the deletion has failed or not. + final bool isDeleteFailed; + + /// The action (deleting the message) to be performed on tap. + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), + child: Row( + children: [ + StreamSvgIcon.delete( + color: Colors.red, + ), + const SizedBox(width: 16), + Text( + context.translations.toggleDeleteRetryDeleteMessageText( + isDeleteFailed: isDeleteFailed, + ), + style: StreamChatTheme.of(context) + .textTheme + .body + .copyWith(color: Colors.red), + ), + ], + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/edit_message_button.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/edit_message_button.dart new file mode 100644 index 00000000..e2cd6310 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal/edit_message_button.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template editMessageButton} +/// Allows a user to edit a message. +/// +/// Used by [MessageActionsModal]. Should not be used by itself. +/// {@endtemplate} +class EditMessageButton extends StatelessWidget { + /// {@macro editMessageButton} + const EditMessageButton({ + super.key, + required this.onTap, + }); + + /// The callback to perform when the button is tapped. + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final streamChatThemeData = StreamChatTheme.of(context); + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), + child: Row( + children: [ + StreamSvgIcon.edit( + color: streamChatThemeData.primaryIconTheme.color, + ), + const SizedBox(width: 16), + Text( + context.translations.editMessageLabel, + style: streamChatThemeData.textTheme.body, + ), + ], + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/flag_message_button.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/flag_message_button.dart new file mode 100644 index 00000000..c64e0a5d --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal/flag_message_button.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template flagMessageButton} +/// Allows a user to flag a message. +/// +/// Used by [MessageActionsModal]. Should not be used by itself. +/// {@endtemplate} +class FlagMessageButton extends StatelessWidget { + /// {@macro flagMessageButton} + const FlagMessageButton({ + super.key, + required this.onTap, + }); + + /// The callback to perform when the button is tapped. + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final streamChatThemeData = StreamChatTheme.of(context); + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), + child: Row( + children: [ + StreamSvgIcon.iconFlag( + color: streamChatThemeData.primaryIconTheme.color, + ), + const SizedBox(width: 16), + Text( + context.translations.flagMessageLabel, + style: streamChatThemeData.textTheme.body, + ), + ], + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/mam_widgets.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/mam_widgets.dart new file mode 100644 index 00000000..e756d682 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal/mam_widgets.dart @@ -0,0 +1,8 @@ +export 'copy_message_button.dart'; +export 'delete_message_button.dart'; +export 'edit_message_button.dart'; +export 'flag_message_button.dart'; +export 'pin_message_button.dart'; +export 'reply_button.dart'; +export 'resend_message_button.dart'; +export 'thread_reply_button.dart'; diff --git a/packages/stream_chat_flutter/lib/src/message_action.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/message_action.dart similarity index 51% rename from packages/stream_chat_flutter/lib/src/message_action.dart rename to packages/stream_chat_flutter/lib/src/message_actions_modal/message_action.dart index edd732f5..f3acaac9 100644 --- a/packages/stream_chat_flutter/lib/src/message_action.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal/message_action.dart @@ -1,15 +1,11 @@ import 'package:flutter/widgets.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_flutter/src/utils/typedefs.dart'; -/// {@macro message_action} -@Deprecated("Use 'StreamMessageActions' instead") -typedef MessageAction = StreamMessageAction; - -/// {@template message_action} +/// {@template streamMessageAction} /// Class describing a message action /// {@endtemplate} class StreamMessageAction { - /// returns a new instance of a [StreamMessageAction] + /// {@macro streamMessageAction} StreamMessageAction({ this.leading, this.title, @@ -22,6 +18,6 @@ class StreamMessageAction { /// title widget final Widget? title; - /// callback called on tap + /// {@macro onMessageTap} final OnMessageTap? onTap; } diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/message_actions_modal.dart new file mode 100644 index 00000000..f59f960a --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal/message_actions_modal.dart @@ -0,0 +1,516 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart' hide ButtonStyle; +import 'package:stream_chat_flutter/src/message_actions_modal/mam_widgets.dart'; +import 'package:stream_chat_flutter/src/utils/utils.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template messageActionsModal} +/// Constructs a modal with actions for a message +/// {@endtemplate} +class MessageActionsModal extends StatefulWidget { + /// {@macro messageActionsModal} + const MessageActionsModal({ + super.key, + required this.message, + required this.messageWidget, + required this.messageTheme, + this.showReactions = true, + this.showDeleteMessage = true, + this.showEditMessage = true, + this.onReplyTap, + this.onThreadReplyTap, + this.showCopyMessage = true, + this.showReplyMessage = true, + this.showResendMessage = true, + this.showThreadReplyMessage = true, + this.showFlagButton = true, + this.showPinButton = true, + this.editMessageInputBuilder, + this.reverse = false, + this.customActions = const [], + this.onCopyTap, + }); + + /// Widget that shows the message + final Widget messageWidget; + + /// Builder for edit message + final EditMessageInputBuilder? editMessageInputBuilder; + + /// The action to perform when "thread reply" is tapped + final OnMessageTap? onThreadReplyTap; + + /// The action to perform when "reply" is tapped + final OnMessageTap? onReplyTap; + + /// Message in focus for actions + final Message message; + + /// [StreamMessageThemeData] for message + final StreamMessageThemeData messageTheme; + + /// Flag for showing reactions + final bool showReactions; + + /// Callback when copy is tapped + final OnMessageTap? onCopyTap; + + /// Callback when delete is tapped + final bool showDeleteMessage; + + /// Flag for showing copy action + final bool showCopyMessage; + + /// Flag for showing edit action + final bool showEditMessage; + + /// Flag for showing resend action + final bool showResendMessage; + + /// Flag for showing reply action + final bool showReplyMessage; + + /// Flag for showing thread reply action + final bool showThreadReplyMessage; + + /// Flag for showing flag action + final bool showFlagButton; + + /// Flag for showing pin action + final bool showPinButton; + + /// Flag for reversing message + final bool reverse; + + /// List of custom actions + final List customActions; + + @override + _MessageActionsModalState createState() => _MessageActionsModalState(); +} + +class _MessageActionsModalState extends State { + bool _showActions = true; + + @override + Widget build(BuildContext context) => _showMessageOptionsModal(); + + Widget _showMessageOptionsModal() { + final mediaQueryData = MediaQuery.of(context); + final size = mediaQueryData.size; + final user = StreamChat.of(context).currentUser; + + final roughMaxSize = size.width * 2 / 3; + var messageTextLength = widget.message.text!.length; + if (widget.message.quotedMessage != null) { + var quotedMessageLength = + (widget.message.quotedMessage!.text?.length ?? 0) + 40; + if (widget.message.quotedMessage!.attachments.isNotEmpty) { + quotedMessageLength += 40; + } + if (quotedMessageLength > messageTextLength) { + messageTextLength = quotedMessageLength; + } + } + final roughSentenceSize = messageTextLength * + (widget.messageTheme.messageTextStyle?.fontSize ?? 1) * + 1.2; + final divFactor = widget.message.attachments.isNotEmpty + ? 1 + : (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize)); + + final streamChatThemeData = StreamChatTheme.of(context); + + final numberOfReactions = + StreamChatConfiguration.of(context).reactionIcons.length; + final shiftFactor = + numberOfReactions < 5 ? (5 - numberOfReactions) * 0.1 : 0.0; + final channel = StreamChannel.of(context).channel; + + final child = Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: widget.reverse + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + if (widget.showReactions && + (widget.message.status == MessageSendingStatus.sent)) + LayoutBuilder( + builder: (context, constraints) { + return Align( + alignment: Alignment( + _calculateReactionsHorizontalAlignmentValue( + user, + divFactor, + shiftFactor, + constraints, + ), + 0, + ), + child: StreamReactionPicker( + message: widget.message, + ), + ); + }, + ), + const SizedBox(height: 8), + IgnorePointer( + child: widget.messageWidget, + ), + const SizedBox(height: 8), + Padding( + padding: EdgeInsets.only( + left: widget.reverse ? 0 : 40, + ), + child: SizedBox( + width: mediaQueryData.size.width * 0.75, + child: Material( + color: streamChatThemeData.colorTheme.appBg, + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (widget.showReplyMessage && + widget.message.status == MessageSendingStatus.sent) + ReplyButton( + onTap: () { + Navigator.of(context).pop(); + if (widget.onReplyTap != null) { + widget.onReplyTap?.call(widget.message); + } + }, + ), + if (widget.showThreadReplyMessage && + (widget.message.status == + MessageSendingStatus.sent) && + widget.message.parentId == null) + ThreadReplyButton( + message: widget.message, + onThreadReplyTap: widget.onThreadReplyTap, + ), + if (widget.showResendMessage) + ResendMessageButton( + message: widget.message, + channel: channel, + ), + if (widget.showEditMessage) + EditMessageButton( + onTap: () { + Navigator.of(context).pop(); + _showEditBottomSheet(context); + }, + ), + if (widget.showCopyMessage) + CopyMessageButton( + onTap: () { + widget.onCopyTap?.call(widget.message); + Navigator.of(context).pop(); + }, + ), + if (widget.showFlagButton) + FlagMessageButton( + onTap: _showFlagDialog, + ), + if (widget.showPinButton) + PinMessageButton( + onTap: _togglePin, + pinned: widget.message.pinned, + ), + if (widget.showDeleteMessage) + DeleteMessageButton( + isDeleteFailed: widget.message.status == + MessageSendingStatus.failed_delete, + onTap: _showDeleteBottomSheet, + ), + ...widget.customActions + .map((action) => _buildCustomAction( + context, + action, + )), + ].insertBetween( + Container( + height: 1, + color: streamChatThemeData.colorTheme.borders, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => Navigator.of(context).maybePop(), + child: Stack( + children: [ + Positioned.fill( + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 10, + sigmaY: 10, + ), + child: ColoredBox( + color: streamChatThemeData.colorTheme.overlay, + ), + ), + ), + if (_showActions) + TweenAnimationBuilder( + tween: Tween(begin: 0, end: 1), + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOutBack, + builder: (context, val, child) => Transform.scale( + scale: val, + child: child, + ), + child: child, + ), + ], + ), + ); + } + + double _calculateReactionsHorizontalAlignmentValue( + User? user, + num divFactor, + double shiftFactor, + BoxConstraints constraints, + ) { + var result = 0.0; + var cont = true; + if (user?.id == widget.message.user?.id) { + if (divFactor >= 1.0) { + // This calculation is hacky and does not cover all bases!!! + // A better option is needed! + + // Landscape calculations + if (constraints.maxWidth == 1350) { + // 12.7 iPad Pro + result = shiftFactor + 0.5; + cont = false; + } else if (constraints.maxWidth == 1178) { + // 11 inch iPad Pro + result = shiftFactor + 0.42; + cont = false; + } else if (constraints.maxWidth == 1164) { + // iPad Air 4 + result = shiftFactor + 0.4; + cont = false; + } else if (constraints.maxWidth == 1117) { + // iPad Mini 6 + result = shiftFactor + 0.37; + cont = false; + } else if (constraints.maxWidth == 1064) { + // iPad 9th gen + result = shiftFactor + 0.33; + cont = false; + } else if (constraints.maxWidth == 1008) { + // 9.7 inch iPad Pro + result = shiftFactor + 0.3; + cont = false; + } else if (constraints.maxWidth >= 200 && constraints.maxWidth <= 400) { + // Phone (?) + result = shiftFactor - 0.2; + cont = false; + } + + if (cont) { + // Portrait calculations + if (constraints.maxWidth == 1008) { + // 12.7 iPad Pro + result = shiftFactor + 0.3; + } else if (constraints.maxWidth == 818) { + // 11 inch iPad Pro + result = shiftFactor + 0.07; + } else if (constraints.maxWidth == 804) { + // iPad Air 4 + result = shiftFactor + 0.04; + } else if (constraints.maxWidth == 794) { + // iPad 9th gen + result = shiftFactor + 0.02; + } else if (constraints.maxWidth >= 752) { + // 9.7 inch iPad Pro + result = shiftFactor - 0.05; + } else if (constraints.maxWidth == 728) { + // iPad Mini 6 + result = shiftFactor - 0.1; + } + } + } else { + result = 1.2 - divFactor; + } + } else { + if (divFactor >= 1.0) { + result = shiftFactor + 0.2; + } else { + result = -(1.2 - divFactor); + } + } + + // Ensure reactions don't get pushed past the edge of the screen. + // + // Hacky!!! Needs improvement!!! + if (result > 1) { + return 1; + } else { + return result; + } + } + + InkWell _buildCustomAction( + BuildContext context, + StreamMessageAction messageAction, + ) { + return InkWell( + onTap: () => messageAction.onTap?.call(widget.message), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), + child: Row( + children: [ + messageAction.leading ?? const Offstage(), + const SizedBox(width: 16), + messageAction.title ?? const Offstage(), + ], + ), + ), + ); + } + + Future _showFlagDialog() async { + final client = StreamChat.of(context).client; + + final streamChatThemeData = StreamChatTheme.of(context); + final answer = await showConfirmationBottomSheet( + context, + title: context.translations.flagMessageLabel, + icon: StreamSvgIcon.flag( + color: streamChatThemeData.colorTheme.accentError, + size: 24, + ), + question: context.translations.flagMessageQuestion, + okText: context.translations.flagLabel, + cancelText: context.translations.cancelLabel, + ); + + final theme = streamChatThemeData; + if (answer == true) { + try { + await client.flagMessage(widget.message.id); + await showInfoBottomSheet( + context, + icon: StreamSvgIcon.flag( + color: theme.colorTheme.accentError, + size: 24, + ), + details: context.translations.flagMessageSuccessfulText, + title: context.translations.flagMessageSuccessfulLabel, + okText: context.translations.okLabel, + ); + } catch (err) { + if (err is StreamChatNetworkError && + err.errorCode == ChatErrorCode.inputError) { + await showInfoBottomSheet( + context, + icon: StreamSvgIcon.flag( + color: theme.colorTheme.accentError, + size: 24, + ), + details: context.translations.flagMessageSuccessfulText, + title: context.translations.flagMessageSuccessfulLabel, + okText: context.translations.okLabel, + ); + } else { + _showErrorAlertBottomSheet(); + } + } + } + } + + Future _togglePin() async { + final channel = StreamChannel.of(context).channel; + + Navigator.of(context).pop(); + try { + if (!widget.message.pinned) { + await channel.pinMessage(widget.message); + } else { + await channel.unpinMessage(widget.message); + } + } catch (e) { + _showErrorAlertBottomSheet(); + } + } + + /// Shows a "delete message" bottom sheet on mobile platforms. + Future _showDeleteBottomSheet() async { + setState(() => _showActions = false); + final answer = await showConfirmationBottomSheet( + context, + title: context.translations.deleteMessageLabel, + icon: StreamSvgIcon.flag( + color: StreamChatTheme.of(context).colorTheme.accentError, + size: 24, + ), + question: context.translations.deleteMessageQuestion, + okText: context.translations.deleteLabel, + cancelText: context.translations.cancelLabel, + ); + + if (answer == true) { + try { + Navigator.of(context).pop(); + await StreamChannel.of(context).channel.deleteMessage(widget.message); + } catch (err) { + _showErrorAlertBottomSheet(); + } + } else { + setState(() => _showActions = true); + } + } + + void _showErrorAlertBottomSheet() { + showInfoBottomSheet( + context, + icon: StreamSvgIcon.error( + color: StreamChatTheme.of(context).colorTheme.accentError, + size: 24, + ), + details: context.translations.operationCouldNotBeCompletedText, + title: context.translations.somethingWentWrongError, + okText: context.translations.okLabel, + ); + } + + void _showEditBottomSheet(BuildContext context) { + final channel = StreamChannel.of(context).channel; + showModalBottomSheet( + context: context, + elevation: 2, + clipBehavior: Clip.hardEdge, + isScrollControlled: true, + backgroundColor: StreamMessageInputTheme.of(context).inputBackgroundColor, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + builder: (context) => EditMessageSheet( + message: widget.message, + channel: channel, + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/pin_message_button.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/pin_message_button.dart new file mode 100644 index 00000000..199a123b --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal/pin_message_button.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template pinMessageButton} +/// Allows a user to pin or unpin a message. +/// +/// Used by [MessageActionsModal]. Should not be used by itself. +/// {@endtemplate} +class PinMessageButton extends StatelessWidget { + /// {@macro pinMessageButton} + const PinMessageButton({ + super.key, + required this.onTap, + required this.pinned, + }); + + /// The callback to perform when the button is tapped. + final VoidCallback onTap; + + /// Whether the selected message is currently pinned or not. + final bool pinned; + + @override + Widget build(BuildContext context) { + final streamChatThemeData = StreamChatTheme.of(context); + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), + child: Row( + children: [ + StreamSvgIcon.pin( + color: streamChatThemeData.primaryIconTheme.color, + size: 24, + ), + const SizedBox(width: 16), + Text( + context.translations.togglePinUnpinText( + pinned: pinned, + ), + style: streamChatThemeData.textTheme.body, + ), + ], + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/reply_button.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/reply_button.dart new file mode 100644 index 00000000..2f30b3b0 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal/reply_button.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template replyButton} +/// Allows a user to reply to a message. +/// +/// Used by [MessageActionsModal]. Should not be used by itself. +/// {@endtemplate} +class ReplyButton extends StatelessWidget { + /// {@macro replyButton} + const ReplyButton({ + super.key, + required this.onTap, + }); + + /// The callback to perform when the button is tapped. + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final streamChatThemeData = StreamChatTheme.of(context); + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), + child: Row( + children: [ + StreamSvgIcon.reply( + color: streamChatThemeData.primaryIconTheme.color, + ), + const SizedBox(width: 16), + Text( + context.translations.replyLabel, + style: streamChatThemeData.textTheme.body, + ), + ], + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/resend_message_button.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/resend_message_button.dart new file mode 100644 index 00000000..c0e12968 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal/resend_message_button.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template resendMessageButton} +/// Allows a user to resend a message that has failed to be sent. +/// +/// Used by [MessageActionsModal]. Should not be used by itself. +/// {@endtemplate} +class ResendMessageButton extends StatelessWidget { + /// {@macro resendMessageButton} + const ResendMessageButton({ + super.key, + required this.message, + required this.channel, + }); + + /// The message to resend. + final Message message; + + /// The [StreamChannel] above this widget. + final Channel channel; + + @override + Widget build(BuildContext context) { + final isUpdateFailed = message.status == MessageSendingStatus.failed_update; + final streamChatThemeData = StreamChatTheme.of(context); + return InkWell( + onTap: () { + Navigator.of(context).pop(); + if (isUpdateFailed) { + channel.updateMessage(message); + } else { + channel.sendMessage(message); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), + child: Row( + children: [ + StreamSvgIcon.circleUp( + color: streamChatThemeData.colorTheme.accentPrimary, + ), + const SizedBox(width: 16), + Text( + context.translations.toggleResendOrResendEditedMessage( + isUpdateFailed: isUpdateFailed, + ), + style: streamChatThemeData.textTheme.body, + ), + ], + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/thread_reply_button.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/thread_reply_button.dart new file mode 100644 index 00000000..00ef07dd --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal/thread_reply_button.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/utils.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template threadReplyButton} +/// Allows a user to start a thread reply to a message. +/// +/// Used by [MessageActionsModal]. Should not be used by itself. +/// {@endtemplate} +class ThreadReplyButton extends StatelessWidget { + /// {@macro threadReplyButton} + const ThreadReplyButton({ + super.key, + required this.message, + this.onThreadReplyTap, + }); + + /// The message to start a thread reply to. + final Message message; + + /// The action to perform when "thread reply" is tapped + final OnMessageTap? onThreadReplyTap; + + @override + Widget build(BuildContext context) { + final streamChatThemeData = StreamChatTheme.of(context); + return InkWell( + onTap: () { + Navigator.of(context).pop(); + if (onThreadReplyTap != null) { + onThreadReplyTap?.call(message); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), + child: Row( + children: [ + StreamSvgIcon.thread( + color: streamChatThemeData.primaryIconTheme.color, + ), + const SizedBox(width: 16), + Text( + context.translations.threadReplyLabel, + style: streamChatThemeData.textTheme.body, + ), + ], + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart deleted file mode 100644 index f58d7bad..00000000 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ /dev/null @@ -1,2086 +0,0 @@ -// ignore_for_file: deprecated_member_use_from_same_package - -import 'dart:async'; -import 'dart:math'; - -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:collection/collection.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:photo_manager/photo_manager.dart'; -import 'package:shimmer/shimmer.dart'; -import 'package:stream_chat_flutter/src/commands_overlay.dart'; -import 'package:stream_chat_flutter/src/emoji/emoji.dart'; -import 'package:stream_chat_flutter/src/emoji_overlay.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/media_list_view.dart'; -import 'package:stream_chat_flutter/src/media_list_view_controller.dart'; -import 'package:stream_chat_flutter/src/quoted_message_widget.dart'; -import 'package:stream_chat_flutter/src/user_mentions_overlay.dart'; -import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// A callback that can be passed to [MessageInput.onError]. -/// -/// This callback should not throw. -/// -/// It exists merely for error reporting, and should not be used otherwise. -typedef ErrorListener = void Function( - Object error, - StackTrace? stackTrace, -); - -/// A callback that can be passed to [MessageInput.onAttachmentLimitExceed]. -/// -/// This callback should not throw. -/// -/// It exists merely for showing custom error, and should not be used otherwise. -typedef AttachmentLimitExceedListener = void Function( - int limit, - String error, -); - -/// Builder for attachment thumbnails -typedef AttachmentThumbnailBuilder = Widget Function( - BuildContext, - Attachment, -); - -/// Builder function for building a mention tile. -typedef MentionTileBuilder = Widget Function( - BuildContext context, - Member member, -); - -/// Builder function for building a user mention tile. -/// -/// Use [StreamUserMentionTile] for the default implementation. -typedef UserMentionTileBuilder = Widget Function( - BuildContext context, - User user, -); - -/// Widget builder for action button. -/// -/// [defaultActionButton] is the default [IconButton] configuration, -/// use .copyWith to easily customize it. -typedef ActionButtonBuilder = Widget Function( - BuildContext context, - IconButton defaultActionButton, -); - -/// Location for actions on the [MessageInput] -enum ActionsLocation { - /// Align to left - left, - - /// Align to right - right, - - /// Align to left but inside the [TextField] - leftInside, - - /// Align to right but inside the [TextField] - rightInside, -} - -/// Default attachments for widget -enum DefaultAttachmentTypes { - /// Image Attachment - image, - - /// Video Attachment - video, - - /// File Attachment - file, -} - -/// Available locations for the sendMessage button relative to the textField -enum SendButtonLocation { - /// inside the textField - inside, - - /// outside the textField - outside, -} - -const _kMinMediaPickerSize = 360.0; - -/// Default maximum size for media attachments. -const kDefaultMaxAttachmentSize = 1024 * 1024 * 100; // 100MB in Bytes - -/// Inactive state -/// -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_input.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_input_paint.png) -/// -/// Focused state -/// -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_input2.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_input2_paint.png) -/// -/// Widget used to enter the message and add attachments -/// -/// ```dart -/// class ChannelPage extends StatelessWidget { -/// const ChannelPage({ -/// Key key, -/// }) : super(key: key); -/// -/// @override -/// Widget build(BuildContext context) { -/// return Scaffold( -/// appBar: ChannelHeader(), -/// body: Column( -/// children: [ -/// Expanded( -/// child: MessageListView( -/// threadBuilder: (_, parentMessage) { -/// return ThreadPage( -/// parent: parentMessage, -/// ); -/// }, -/// ), -/// ), -/// MessageInput(), -/// ], -/// ), -/// ); -/// } -/// } -/// ``` -/// -/// You usually put this widget in the same page of a [StreamMessageListView] -/// as the bottom widget. -/// -/// The widget renders the ui based on the first ancestor of -/// type [StreamChatTheme]. -/// Modify it to change the widget appearance. -@Deprecated("Use 'StreamMessageInput' instead") -class MessageInput extends StatefulWidget { - /// Instantiate a new MessageInput - const MessageInput({ - super.key, - this.onMessageSent, - this.preMessageSending, - this.parentMessage, - this.editMessage, - this.maxHeight = 150, - this.keyboardType = TextInputType.multiline, - this.disableAttachments = false, - this.initialMessage, - this.textEditingController, - this.actions = const [], - this.actionsLocation = ActionsLocation.left, - this.attachmentThumbnailBuilders, - this.focusNode, - this.quotedMessage, - this.onQuotedMessageCleared, - this.sendButtonLocation = SendButtonLocation.outside, - this.autofocus = false, - this.hideSendAsDm = false, - this.idleSendButton, - this.activeSendButton, - this.showCommandsButton = true, - @Deprecated('''Use `userMentionsTileBuilder` instead. Will be removed in future release''') - this.mentionsTileBuilder, - this.userMentionsTileBuilder, - this.maxAttachmentSize = kDefaultMaxAttachmentSize, - this.onError, - this.attachmentLimit = 10, - this.onAttachmentLimitExceed, - this.attachmentButtonBuilder, - this.commandButtonBuilder, - this.customOverlays = const [], - this.mentionAllAppUsers = false, - this.shouldKeepFocusAfterMessage, - }) : assert( - initialMessage == null || editMessage == null, - "Can't provide both `initialMessage` and `editMessage`", - ); - - /// List of options for showing overlays - final List customOverlays; - - /// Message to edit - final Message? editMessage; - - /// Max attachment size in bytes - /// Defaults to 20 MB - /// do not set it if you're using our default CDN - final int maxAttachmentSize; - - /// Message to start with - final Message? initialMessage; - - /// Function called after sending the message - final void Function(Message)? onMessageSent; - - /// Function called right before sending the message - /// Use this to transform the message - final FutureOr Function(Message)? preMessageSending; - - /// Parent message in case of a thread - final Message? parentMessage; - - /// Maximum Height for the TextField to grow before it starts scrolling - final double maxHeight; - - /// The keyboard type assigned to the TextField - final TextInputType keyboardType; - - /// If true the attachments button will not be displayed - final bool disableAttachments; - - /// Use this property to hide/show the commands button - final bool showCommandsButton; - - /// Hide send as dm checkbox - final bool hideSendAsDm; - - /// The text controller of the TextField - final TextEditingController? textEditingController; - - /// List of action widgets - final List actions; - - /// The location of the custom actions - final ActionsLocation actionsLocation; - - /// Map that defines a thumbnail builder for an attachment type - final Map? attachmentThumbnailBuilders; - - /// The focus node associated to the TextField - final FocusNode? focusNode; - - /// - final Message? quotedMessage; - - /// - final VoidCallback? onQuotedMessageCleared; - - /// The location of the send button - final SendButtonLocation sendButtonLocation; - - /// Autofocus property passed to the TextField - final bool autofocus; - - /// Send button widget in an idle state - final Widget? idleSendButton; - - /// Send button widget in an active state - final Widget? activeSendButton; - - /// Customize the tile for the mentions overlay. - final MentionTileBuilder? mentionsTileBuilder; - - /// Customize the tile for the mentions overlay. - final UserMentionTileBuilder? userMentionsTileBuilder; - - /// A callback for error reporting - final ErrorListener? onError; - - /// A limit for the no. of attachments that can be sent with a single message. - final int attachmentLimit; - - /// A callback for when the [attachmentLimit] is exceeded. - /// - /// This will override the default error alert behaviour. - final AttachmentLimitExceedListener? onAttachmentLimitExceed; - - /// Builder for customizing the attachment button. - /// - /// The builder contains the default [IconButton] that can be customized by - /// calling `.copyWith`. - final ActionButtonBuilder? attachmentButtonBuilder; - - /// Builder for customizing the command button. - /// - /// The builder contains the default [IconButton] that can be customized by - /// calling `.copyWith`. - final ActionButtonBuilder? commandButtonBuilder; - - /// When enabled mentions search users across the entire app. - /// - /// Defaults to false. - final bool mentionAllAppUsers; - - /// Defines if the [MessageInput] loses focuses after a message is sent. - /// The default behaviour keeps focus until a command is enabled. - final bool? shouldKeepFocusAfterMessage; - - @override - MessageInputState createState() => MessageInputState(); - - /// Use this method to get the current [StreamChatState] instance - static MessageInputState of(BuildContext context) { - MessageInputState? messageInputState; - messageInputState = context.findAncestorStateOfType(); - assert( - messageInputState != null, - 'You must have a MessageInput widget as ancestor of your widget tree', - ); - return messageInputState!; - } -} - -/// State of [MessageInput] -@Deprecated("Use 'StreamMessageInput' instead") -class MessageInputState extends State { - final _attachments = {}; - final List _mentionedUsers = []; - - final _imagePicker = ImagePicker(); - final _mediaListViewController = MediaListViewController(); - late final _focusNode = widget.focusNode ?? FocusNode(); - late final _isInternalFocusNode = widget.focusNode == null; - bool _inputEnabled = true; - bool _commandEnabled = false; - bool _showCommandsOverlay = false; - bool _showMentionsOverlay = false; - - Command? _chosenCommand; - bool _actionsShrunk = false; - bool _sendAsDm = false; - bool _openFilePickerSection = false; - int _filePickerIndex = 0; - - /// The editing controller passed to the input TextField - late final TextEditingController textEditingController = - widget.textEditingController ?? TextEditingController(); - - late StreamChatThemeData _streamChatTheme; - late StreamMessageInputThemeData _messageInputTheme; - - bool get _hasQuotedMessage => widget.quotedMessage != null; - - bool get _messageIsPresent => textEditingController.text.trim().isNotEmpty; - - @override - void initState() { - super.initState(); - if (widget.editMessage != null || widget.initialMessage != null) { - _parseExistingMessage(widget.editMessage ?? widget.initialMessage!); - } - textEditingController.addListener(_onChangedDebounced); - _focusNode.addListener(_focusNodeListener); - } - - void _focusNodeListener() { - if (_focusNode.hasFocus) { - _openFilePickerSection = false; - } - } - - int _timeOut = 0; - Timer? _slowModeTimer; - - void _startSlowMode() { - if (!mounted) { - return; - } - final channel = StreamChannel.of(context).channel; - final cooldownStartedAt = channel.cooldownStartedAt; - if (cooldownStartedAt != null) { - final diff = DateTime.now().difference(cooldownStartedAt).inSeconds; - if (diff < channel.cooldown) { - _timeOut = channel.cooldown - diff; - if (_timeOut > 0) { - _slowModeTimer = Timer.periodic(const Duration(seconds: 1), (timer) { - if (_timeOut == 0) { - timer.cancel(); - } else { - if (mounted) { - setState(() => _timeOut -= 1); - } - } - }); - } - } - } - } - - void _stopSlowMode() => _slowModeTimer?.cancel(); - - @override - Widget build(BuildContext context) { - Widget child = DecoratedBox( - decoration: BoxDecoration( - color: _messageInputTheme.inputBackgroundColor, - ), - child: SafeArea( - child: GestureDetector( - onPanUpdate: (details) { - if (details.delta.dy > 0) { - _focusNode.unfocus(); - if (_openFilePickerSection) { - setState(() { - _openFilePickerSection = false; - }); - } - } - }, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (_hasQuotedMessage) - Padding( - padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.all(8), - child: StreamSvgIcon.reply( - color: _streamChatTheme.colorTheme.disabled, - ), - ), - Text( - context.translations.replyToMessageLabel, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - IconButton( - visualDensity: VisualDensity.compact, - icon: StreamSvgIcon.closeSmall(), - onPressed: widget.onQuotedMessageCleared, - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: _buildTextField(context), - ), - if (widget.parentMessage != null && !widget.hideSendAsDm) - Padding( - padding: const EdgeInsets.only( - right: 12, - left: 12, - bottom: 12, - ), - child: _buildDmCheckbox(), - ), - _buildFilePickerSection(), - ], - ), - ), - ), - ); - if (widget.editMessage == null) { - child = Material( - elevation: 8, - color: _messageInputTheme.inputBackgroundColor, - child: child, - ); - } - - return StreamMultiOverlay( - childAnchor: Alignment.topCenter, - overlayAnchor: Alignment.bottomCenter, - overlayOptions: [ - OverlayOptions( - visible: _showCommandsOverlay, - widget: _buildCommandsOverlayEntry(), - ), - OverlayOptions( - visible: _focusNode.hasFocus && - textEditingController.text.isNotEmpty && - textEditingController.selection.baseOffset > 0 && - textEditingController.text - .substring( - 0, - textEditingController.selection.baseOffset, - ) - .contains(':'), - widget: _buildEmojiOverlay(), - ), - OverlayOptions( - visible: _showMentionsOverlay, - widget: _buildMentionsOverlayEntry(), - ), - ...widget.customOverlays, - ], - child: child, - ); - } - - Flex _buildTextField(BuildContext context) => Flex( - direction: Axis.horizontal, - children: [ - if (!_commandEnabled && - widget.actionsLocation == ActionsLocation.left) - _buildExpandActionsButton(context), - _buildTextInput(context), - if (!_commandEnabled && - widget.actionsLocation == ActionsLocation.right) - _buildExpandActionsButton(context), - if (widget.sendButtonLocation == SendButtonLocation.outside) - _animateSendButton(context), - ], - ); - - Widget _buildDmCheckbox() => Row( - children: [ - Container( - height: 16, - width: 16, - foregroundDecoration: BoxDecoration( - border: _sendAsDm - ? null - : Border.all( - color: _streamChatTheme.colorTheme.textHighEmphasis - .withOpacity(0.5), - width: 2, - ), - borderRadius: BorderRadius.circular(3), - ), - child: Center( - child: Material( - borderRadius: BorderRadius.circular(3), - color: _sendAsDm - ? _streamChatTheme.colorTheme.accentPrimary - : _streamChatTheme.colorTheme.barsBg, - child: InkWell( - onTap: () { - setState(() { - _sendAsDm = !_sendAsDm; - }); - }, - child: AnimatedCrossFade( - duration: const Duration(milliseconds: 300), - reverseDuration: const Duration(milliseconds: 300), - crossFadeState: _sendAsDm - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - firstChild: StreamSvgIcon.check( - size: 16, - color: _streamChatTheme.colorTheme.barsBg, - ), - secondChild: const SizedBox( - height: 16, - width: 16, - ), - ), - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Text( - context.translations.alsoSendAsDirectMessageLabel, - style: _streamChatTheme.textTheme.footnote.copyWith( - color: _streamChatTheme.colorTheme.textHighEmphasis - .withOpacity(0.5), - ), - ), - ), - ], - ); - - Widget _animateSendButton(BuildContext context) { - late Widget sendButton; - if (_timeOut > 0) { - sendButton = _CountdownButton(count: _timeOut); - } else if (!_messageIsPresent && _attachments.isEmpty) { - sendButton = widget.idleSendButton ?? _buildIdleSendButton(context); - } else { - sendButton = widget.activeSendButton != null - ? InkWell( - onTap: sendMessage, - child: widget.activeSendButton, - ) - : _buildSendButton(context); - } - - return AnimatedSwitcher( - duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!, - child: sendButton, - ); - } - - Widget _buildExpandActionsButton(BuildContext context) { - final channel = StreamChannel.of(context).channel; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: AnimatedCrossFade( - crossFadeState: _actionsShrunk - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - firstCurve: Curves.easeOut, - secondCurve: Curves.easeIn, - firstChild: IconButton( - onPressed: () { - if (_actionsShrunk) { - setState(() => _actionsShrunk = false); - } - }, - icon: Transform.rotate( - angle: (widget.actionsLocation == ActionsLocation.right || - widget.actionsLocation == ActionsLocation.rightInside) - ? pi - : 0, - child: StreamSvgIcon.emptyCircleLeft( - color: _messageInputTheme.expandButtonColor, - ), - ), - padding: EdgeInsets.zero, - constraints: const BoxConstraints.tightFor( - height: 24, - width: 24, - ), - splashRadius: 24, - ), - secondChild: widget.disableAttachments && - !widget.showCommandsButton && - !widget.actions.isNotEmpty - ? const Offstage() - : Wrap( - children: [ - if (!widget.disableAttachments) - _buildAttachmentButton(context), - if (widget.showCommandsButton && - widget.editMessage == null && - channel.state != null && - channel.config?.commands.isNotEmpty == true) - _buildCommandButton(context), - ...widget.actions, - ].insertBetween(const SizedBox(width: 8)), - ), - duration: const Duration(milliseconds: 300), - alignment: Alignment.center, - ), - ); - } - - Expanded _buildTextInput(BuildContext context) { - final margin = (widget.sendButtonLocation == SendButtonLocation.inside - ? const EdgeInsets.only(right: 8) - : EdgeInsets.zero) + - (widget.actionsLocation != ActionsLocation.left || _commandEnabled - ? const EdgeInsets.only(left: 8) - : EdgeInsets.zero); - return Expanded( - child: Container( - clipBehavior: Clip.hardEdge, - margin: margin, - decoration: BoxDecoration( - borderRadius: _messageInputTheme.borderRadius, - gradient: _focusNode.hasFocus - ? _messageInputTheme.activeBorderGradient - : _messageInputTheme.idleBorderGradient, - color: _messageInputTheme.inputBackgroundColor, - ), - child: Padding( - padding: const EdgeInsets.all(1.5), - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: _messageInputTheme.borderRadius, - color: _messageInputTheme.inputBackgroundColor, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildReplyToMessage(), - _buildAttachments(), - LimitedBox( - maxHeight: widget.maxHeight, - child: TextField( - key: const Key('messageInputText'), - enabled: _inputEnabled, - maxLines: null, - onSubmitted: (_) => sendMessage(), - keyboardType: widget.keyboardType, - controller: textEditingController, - focusNode: _focusNode, - style: _messageInputTheme.inputTextStyle, - autofocus: widget.autofocus, - textAlignVertical: TextAlignVertical.center, - decoration: _getInputDecoration(context), - textCapitalization: TextCapitalization.sentences, - ), - ), - ], - ), - ), - ), - ), - ); - } - - InputDecoration _getInputDecoration(BuildContext context) { - final passedDecoration = _messageInputTheme.inputDecoration; - return InputDecoration( - isDense: true, - hintText: _getHint(context), - hintStyle: _messageInputTheme.inputTextStyle!.copyWith( - color: _streamChatTheme.colorTheme.textLowEmphasis, - ), - border: const OutlineInputBorder( - borderSide: BorderSide( - color: Colors.transparent, - ), - ), - focusedBorder: const OutlineInputBorder( - borderSide: BorderSide( - color: Colors.transparent, - ), - ), - enabledBorder: const OutlineInputBorder( - borderSide: BorderSide( - color: Colors.transparent, - ), - ), - errorBorder: const OutlineInputBorder( - borderSide: BorderSide( - color: Colors.transparent, - ), - ), - disabledBorder: const OutlineInputBorder( - borderSide: BorderSide( - color: Colors.transparent, - ), - ), - contentPadding: const EdgeInsets.fromLTRB(16, 12, 13, 11), - prefixIcon: _commandEnabled - ? Row( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.all(8), - child: Container( - constraints: BoxConstraints.tight(const Size(64, 24)), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: _streamChatTheme.colorTheme.accentPrimary, - ), - alignment: Alignment.center, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - StreamSvgIcon.lightning( - color: Colors.white, - size: 16, - ), - Text( - _chosenCommand?.name.toUpperCase() ?? '', - style: - _streamChatTheme.textTheme.footnoteBold.copyWith( - color: Colors.white, - ), - ), - ], - ), - ), - ), - ], - ) - : (widget.actionsLocation == ActionsLocation.leftInside - ? Row( - mainAxisSize: MainAxisSize.min, - children: [_buildExpandActionsButton(context)], - ) - : null), - suffixIconConstraints: const BoxConstraints.tightFor(height: 40), - prefixIconConstraints: const BoxConstraints.tightFor(height: 40), - suffixIcon: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (_commandEnabled) - Padding( - padding: const EdgeInsets.only(right: 8), - child: IconButton( - icon: StreamSvgIcon.closeSmall(), - splashRadius: 24, - padding: EdgeInsets.zero, - constraints: const BoxConstraints.tightFor( - height: 24, - width: 24, - ), - onPressed: () { - setState(() => _commandEnabled = false); - }, - ), - ), - if (!_commandEnabled && - widget.actionsLocation == ActionsLocation.rightInside) - _buildExpandActionsButton(context), - if (widget.sendButtonLocation == SendButtonLocation.inside) - _animateSendButton(context), - ], - ), - ).merge(passedDecoration); - } - - late final _onChangedDebounced = debounce( - () { - var value = textEditingController.text; - if (!mounted) return; - value = value.trim(); - - final channel = StreamChannel.of(context).channel; - if (value.isNotEmpty && - channel.ownCapabilities.contains(PermissionType.sendTypingEvents)) { - // Notify the server that the user started typing. - channel.keyStroke(widget.parentMessage?.id).onError( - (error, stackTrace) { - widget.onError?.call(error!, stackTrace); - }, - ); - } - - var actionsLength = widget.actions.length; - if (widget.showCommandsButton) actionsLength += 1; - if (!widget.disableAttachments) actionsLength += 1; - - setState(() { - _actionsShrunk = value.isNotEmpty && actionsLength > 1; - }); - - _checkCommands(value, context); - _checkMentions(value, context); - _checkEmoji(value, context); - }, - const Duration(milliseconds: 350), - leading: true, - ); - - String _getHint(BuildContext context) { - if (_commandEnabled && _chosenCommand!.name == 'giphy') { - return context.translations.searchGifLabel; - } - if (_attachments.isNotEmpty) { - return context.translations.addACommentOrSendLabel; - } - if (_timeOut != 0) { - return context.translations.slowModeOnLabel; - } - - return context.translations.writeAMessageLabel; - } - - void _checkEmoji(String s, BuildContext context) { - if (s.isNotEmpty && - textEditingController.selection.baseOffset > 0 && - textEditingController.text - .substring(0, textEditingController.selection.baseOffset) - .contains(':')) { - final textToSelection = textEditingController.text - .substring(0, textEditingController.value.selection.start); - final splits = textToSelection.split(':'); - final query = splits[splits.length - 2].toLowerCase(); - final emoji = Emoji.byName(query); - - if (textToSelection.endsWith(':') && emoji != null) { - _chooseEmoji(splits.sublist(0, splits.length - 1), emoji); - } - } - } - - void _checkMentions(String s, BuildContext context) { - if (s.isNotEmpty && - textEditingController.selection.baseOffset > 0 && - textEditingController.text - .substring(0, textEditingController.selection.baseOffset) - .split(' ') - .last - .contains('@')) { - if (!_showMentionsOverlay) { - setState(() { - _showMentionsOverlay = true; - }); - } - } else if (_showMentionsOverlay) { - setState(() { - _showMentionsOverlay = false; - }); - } - } - - void _checkCommands(String s, BuildContext context) { - if (s.startsWith('/')) { - final allCommands = StreamChannel.of(context).channel.config?.commands; - final command = - allCommands?.firstWhereOrNull((it) => it.name == s.substring(1)); - if (command != null) { - return _setCommand(command); - } else if (!_showCommandsOverlay) { - setState(() { - _showCommandsOverlay = true; - }); - } - } else if (_showCommandsOverlay) { - setState(() { - _showCommandsOverlay = false; - }); - } - } - - Widget _buildCommandsOverlayEntry() { - final text = textEditingController.text.trimLeft(); - - final renderObject = context.findRenderObject() as RenderBox?; - if (renderObject == null) { - return const Offstage(); - } - return StreamCommandsOverlay( - channel: StreamChannel.of(context).channel, - size: Size(renderObject.size.width - 16, 400), - text: text, - onCommandResult: _setCommand, - ); - } - - Widget _buildFilePickerSection() { - final _attachmentContainsFile = - _attachments.values.any((it) => it.type == 'file'); - - final attachmentLimitCrossed = - _attachments.length >= widget.attachmentLimit; - - Color _getIconColor(int index) { - final streamChatThemeData = _streamChatTheme; - switch (index) { - case 0: - return _attachments.isEmpty - ? streamChatThemeData.colorTheme.accentPrimary - : (!_attachmentContainsFile - ? streamChatThemeData.colorTheme.accentPrimary - : streamChatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.2)); - case 1: - return _attachmentContainsFile - ? streamChatThemeData.colorTheme.accentPrimary - : (_attachments.isEmpty - ? streamChatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.5) - : streamChatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.2)); - case 2: - return attachmentLimitCrossed - ? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2) - : _attachmentContainsFile && _attachments.isNotEmpty - ? streamChatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.2) - : streamChatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.5); - case 3: - return attachmentLimitCrossed - ? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2) - : _attachmentContainsFile && _attachments.isNotEmpty - ? streamChatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.2) - : streamChatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.5); - default: - return Colors.black; - } - } - - return AnimatedContainer( - duration: _openFilePickerSection - ? const Duration(milliseconds: 300) - : Duration.zero, - curve: Curves.easeOut, - height: _openFilePickerSection ? _kMinMediaPickerSize : 0, - child: SingleChildScrollView( - child: SizedBox( - height: _kMinMediaPickerSize, - child: Material( - color: _streamChatTheme.colorTheme.inputBg, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - IconButton( - icon: StreamSvgIcon.pictures( - color: _getIconColor(0), - ), - onPressed: - _attachmentContainsFile && _attachments.isNotEmpty - ? null - : () { - setState(() { - _filePickerIndex = 0; - }); - }, - ), - IconButton( - iconSize: 32, - icon: StreamSvgIcon.files( - color: _getIconColor(1), - ), - onPressed: - !_attachmentContainsFile && _attachments.isNotEmpty - ? null - : () { - pickFile(DefaultAttachmentTypes.file); - }, - ), - IconButton( - icon: StreamSvgIcon.camera( - color: _getIconColor(2), - ), - onPressed: attachmentLimitCrossed || - (_attachmentContainsFile && - _attachments.isNotEmpty) - ? null - : () { - pickFile( - DefaultAttachmentTypes.image, - camera: true, - ); - }, - ), - IconButton( - padding: EdgeInsets.zero, - icon: StreamSvgIcon.record( - color: _getIconColor(3), - ), - onPressed: attachmentLimitCrossed || - (_attachmentContainsFile && - _attachments.isNotEmpty) - ? null - : () { - pickFile( - DefaultAttachmentTypes.video, - camera: true, - ); - }, - ), - const Spacer(), - FutureBuilder( - future: PhotoManager.requestPermissionExtend(), - builder: (context, snapshot) { - if (snapshot.hasData && - snapshot.data == PermissionState.limited) { - return TextButton( - child: Text(context.translations.viewLibrary), - onPressed: () async { - await PhotoManager.presentLimited(); - _mediaListViewController.updateMedia( - newValue: true, - ); - }, - ); - } - - return const SizedBox.shrink(); - }, - ), - ], - ), - DecoratedBox( - decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.barsBg, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - ), - child: Center( - child: Padding( - padding: const EdgeInsets.all(8), - child: Container( - width: 40, - height: 4, - decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.inputBg, - borderRadius: BorderRadius.circular(4), - ), - ), - ), - ), - ), - if (_openFilePickerSection) - Expanded( - child: DecoratedBox( - decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.barsBg, - borderRadius: BorderRadius.circular(8), - ), - child: _PickerWidget( - mediaListViewController: _mediaListViewController, - filePickerIndex: _filePickerIndex, - streamChatTheme: _streamChatTheme, - containsFile: _attachmentContainsFile, - selectedMedias: _attachments.keys.toList(), - onAddMoreFilesClick: pickFile, - onMediaSelected: (media) { - if (_attachments.containsKey(media.id)) { - setState(() => _attachments.remove(media.id)); - } else { - _addAssetAttachment(media); - } - }, - ), - ), - ), - ], - ), - ), - ), - ), - ); - } - - void _addAssetAttachment(AssetEntity medium) async { - final mediaFile = await medium.originFile.timeout( - const Duration(seconds: 5), - onTimeout: () => medium.originFile, - ); - - if (mediaFile == null) return; - - final file = AttachmentFile( - path: mediaFile.path, - size: await mediaFile.length(), - bytes: mediaFile.readAsBytesSync(), - ); - - if (file.size! > widget.maxAttachmentSize) { - return _showErrorAlert( - context.translations.fileTooLargeError( - widget.maxAttachmentSize / (1024 * 1024), - ), - ); - } - - setState(() { - final attachment = Attachment( - id: medium.id, - file: file, - type: medium.type == AssetType.image ? 'image' : 'video', - ); - _addAttachments([attachment]); - }); - } - - Widget _buildMentionsOverlayEntry() { - final channel = StreamChannel.of(context).channel; - if (textEditingController.value.selection.start < 0 || - channel.state == null) { - return const Offstage(); - } - - final splits = textEditingController.text - .substring(0, textEditingController.value.selection.start) - .split('@'); - final query = splits.last.toLowerCase(); - - // ignore: cast_nullable_to_non_nullable - final renderObject = context.findRenderObject() as RenderBox; - - var tileBuilder = widget.userMentionsTileBuilder; - if (tileBuilder == null && widget.mentionsTileBuilder != null) { - tileBuilder = (context, user) { - final member = Member( - user: user, - userId: user.id, - createdAt: user.createdAt, - updatedAt: user.updatedAt, - ); - return widget.mentionsTileBuilder!(context, member); - }; - } - - return LayoutBuilder( - builder: (context, snapshot) => StreamUserMentionsOverlay( - query: query, - mentionAllAppUsers: widget.mentionAllAppUsers, - client: StreamChat.of(context).client, - channel: channel, - size: Size( - renderObject.size.width - 16, - min(400, (snapshot.maxHeight - renderObject.size.height - 16).abs()), - ), - mentionsTileBuilder: tileBuilder, - onMentionUserTap: (user) { - _mentionedUsers.add(user); - splits[splits.length - 1] = user.name; - final rejoin = splits.join('@'); - - textEditingController.value = TextEditingValue( - text: rejoin + - textEditingController.text.substring( - textEditingController.selection.start, - ), - selection: TextSelection.collapsed( - offset: rejoin.length, - ), - ); - _onChangedDebounced.cancel(); - - setState(() => _showMentionsOverlay = false); - }, - ), - ); - } - - Widget _buildEmojiOverlay() { - if (textEditingController.value.selection.baseOffset < 0) { - return const Offstage(); - } - - final splits = textEditingController.text - .substring(0, textEditingController.value.selection.baseOffset) - .split(':'); - - final query = splits.last.toLowerCase(); - // ignore: cast_nullable_to_non_nullable - final renderObject = context.findRenderObject() as RenderBox; - - return StreamEmojiOverlay( - size: Size(renderObject.size.width - 16, 200), - query: query, - onEmojiResult: (emoji) { - _chooseEmoji(splits, emoji); - }, - ); - } - - void _chooseEmoji(List splits, Emoji emoji) { - final rejoin = splits.sublist(0, splits.length - 1).join(':') + emoji.char!; - - textEditingController.value = TextEditingValue( - text: rejoin + - textEditingController.text - .substring(textEditingController.selection.start), - selection: TextSelection.collapsed( - offset: rejoin.length, - ), - ); - } - - void _setCommand(Command c) { - textEditingController.clear(); - setState(() { - _chosenCommand = c; - _commandEnabled = true; - _showCommandsOverlay = false; - }); - } - - Widget _buildReplyToMessage() { - if (!_hasQuotedMessage) return const Offstage(); - final containsUrl = widget.quotedMessage!.attachments - .any((element) => element.ogScrapeUrl != null); - return StreamQuotedMessageWidget( - reverse: true, - showBorder: !containsUrl, - message: widget.quotedMessage!, - messageTheme: _streamChatTheme.otherMessageTheme, - padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), - ); - } - - Widget _buildAttachments() { - if (_attachments.isEmpty) return const Offstage(); - final fileAttachments = _attachments.values - .where((it) => it.type == 'file') - .toList(growable: false); - final remainingAttachments = _attachments.values - .where((it) => it.type != 'file') - .toList(growable: false); - return Column( - children: [ - if (fileAttachments.isNotEmpty) - Padding( - padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), - child: LimitedBox( - maxHeight: 136, - child: ListView( - reverse: true, - shrinkWrap: true, - children: fileAttachments.reversed - .map( - (e) => ClipRRect( - borderRadius: BorderRadius.circular(10), - child: StreamFileAttachment( - message: Message(), // dummy message - attachment: e, - size: Size( - MediaQuery.of(context).size.width * 0.65, - 56, - ), - trailing: Padding( - padding: const EdgeInsets.all(8), - child: _buildRemoveButton(e), - ), - ), - ), - ) - .insertBetween(const SizedBox(height: 8)), - ), - ), - ), - if (remainingAttachments.isNotEmpty) - Padding( - padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), - child: LimitedBox( - maxHeight: 104, - child: ListView( - scrollDirection: Axis.horizontal, - children: remainingAttachments - .map( - (attachment) => ClipRRect( - borderRadius: BorderRadius.circular(10), - child: Stack( - children: [ - AspectRatio( - aspectRatio: 1, - child: SizedBox( - height: 104, - width: 104, - child: _buildAttachment(attachment), - ), - ), - Positioned( - top: 8, - right: 8, - child: _buildRemoveButton(attachment), - ), - ], - ), - ), - ) - .insertBetween(const SizedBox(width: 8)), - ), - ), - ), - ], - ); - } - - Widget _buildRemoveButton(Attachment attachment) => SizedBox( - height: 24, - width: 24, - child: RawMaterialButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - elevation: 0, - highlightElevation: 0, - focusElevation: 0, - hoverElevation: 0, - onPressed: () { - setState(() => _attachments.remove(attachment.id)); - }, - fillColor: - _streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.5), - child: Center( - child: StreamSvgIcon.close( - size: 24, - color: _streamChatTheme.colorTheme.barsBg, - ), - ), - ), - ); - - Widget _buildAttachment(Attachment attachment) { - if (widget.attachmentThumbnailBuilders?.containsKey(attachment.type) == - true) { - return widget.attachmentThumbnailBuilders![attachment.type!]!( - context, - attachment, - ); - } - - switch (attachment.type) { - case 'image': - case 'giphy': - return attachment.file != null - ? Image.memory( - attachment.file!.bytes!, - fit: BoxFit.cover, - errorBuilder: (context, _, __) => Image.asset( - 'images/placeholder.png', - package: 'stream_chat_flutter', - ), - ) - : CachedNetworkImage( - imageUrl: attachment.imageUrl ?? - attachment.assetUrl ?? - attachment.thumbUrl!, - fit: BoxFit.cover, - errorWidget: (_, obj, trace) => - getFileTypeImage(attachment.extraData['other'] as String?), - placeholder: (context, _) => Shimmer.fromColors( - baseColor: _streamChatTheme.colorTheme.disabled, - highlightColor: _streamChatTheme.colorTheme.inputBg, - child: Image.asset( - 'images/placeholder.png', - fit: BoxFit.cover, - package: 'stream_chat_flutter', - ), - ), - ); - case 'video': - return Stack( - children: [ - StreamVideoThumbnailImage( - height: 104, - width: 104, - video: (attachment.file?.path ?? attachment.assetUrl)!, - fit: BoxFit.cover, - ), - Positioned( - left: 8, - bottom: 10, - child: SvgPicture.asset( - 'svgs/video_call_icon.svg', - package: 'stream_chat_flutter', - ), - ), - ], - ); - default: - return const ColoredBox( - color: Colors.black26, - child: Icon(Icons.insert_drive_file), - ); - } - } - - Widget _buildCommandButton(BuildContext context) { - final s = textEditingController.text.trim(); - final defaultButton = IconButton( - icon: StreamSvgIcon.lightning( - color: s.isNotEmpty - ? _streamChatTheme.colorTheme.disabled - : (_showCommandsOverlay - ? _messageInputTheme.actionButtonColor - : _messageInputTheme.actionButtonIdleColor), - ), - padding: EdgeInsets.zero, - constraints: const BoxConstraints.tightFor( - height: 24, - width: 24, - ), - splashRadius: 24, - onPressed: () async { - if (_openFilePickerSection) { - setState(() => _openFilePickerSection = false); - await Future.delayed(const Duration(milliseconds: 300)); - } - - setState(() { - _showCommandsOverlay = !_showCommandsOverlay; - }); - }, - ); - - return widget.commandButtonBuilder?.call(context, defaultButton) ?? - defaultButton; - } - - Widget _buildAttachmentButton(BuildContext context) { - final defaultButton = IconButton( - icon: StreamSvgIcon.attach( - color: _openFilePickerSection - ? _messageInputTheme.actionButtonColor - : _messageInputTheme.actionButtonIdleColor, - ), - padding: EdgeInsets.zero, - constraints: const BoxConstraints.tightFor( - height: 24, - width: 24, - ), - splashRadius: 24, - onPressed: () async { - _showCommandsOverlay = false; - _showMentionsOverlay = false; - - if (_openFilePickerSection) { - setState(() => _openFilePickerSection = false); - } else { - showAttachmentModal(); - } - }, - ); - - return widget.attachmentButtonBuilder?.call(context, defaultButton) ?? - defaultButton; - } - - /// Show the attachment modal, making the user choose where to - /// pick a media from - void showAttachmentModal() { - if (_focusNode.hasFocus) { - _focusNode.unfocus(); - } - - if (!kIsWeb) { - setState(() { - _openFilePickerSection = true; - }); - } else { - showModalBottomSheet( - clipBehavior: Clip.hardEdge, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(32), - topRight: Radius.circular(32), - ), - ), - context: context, - isScrollControlled: true, - builder: (_) => Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - title: Text( - context.translations.addAFileLabel, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - ListTile( - leading: const Icon(Icons.image), - title: Text(context.translations.uploadAPhotoLabel), - onTap: () { - pickFile(DefaultAttachmentTypes.image); - Navigator.pop(context); - }, - ), - ListTile( - leading: const Icon(Icons.video_library), - title: Text(context.translations.uploadAVideoLabel), - onTap: () { - pickFile(DefaultAttachmentTypes.video); - Navigator.pop(context); - }, - ), - ListTile( - leading: const Icon(Icons.insert_drive_file), - title: Text(context.translations.uploadAFileLabel), - onTap: () { - pickFile(DefaultAttachmentTypes.file); - Navigator.pop(context); - }, - ), - ], - ), - ); - } - } - - /// Add an attachment to the sending message - /// Use this to add custom type attachments - /// - /// Note: Only meant to be used from outside the state. - void addAttachment(Attachment attachment) { - setState(() => _addAttachments([attachment])); - } - - /// Adds an attachment to the [_attachments] map - void _addAttachments(Iterable attachments) { - final limit = widget.attachmentLimit; - final length = _attachments.length + attachments.length; - if (length > limit) { - final onAttachmentLimitExceed = widget.onAttachmentLimitExceed; - if (onAttachmentLimitExceed != null) { - return onAttachmentLimitExceed( - widget.attachmentLimit, - context.translations.attachmentLimitExceedError(limit), - ); - } - return _showErrorAlert( - context.translations.attachmentLimitExceedError(limit), - ); - } - for (final attachment in attachments) { - _attachments[attachment.id] = attachment; - } - } - - /// Pick a file from the device - /// If [camera] is true then the camera will open - void pickFile( - DefaultAttachmentTypes fileType, { - bool camera = false, - }) async { - setState(() => _inputEnabled = false); - - AttachmentFile? file; - String? attachmentType; - - if (fileType == DefaultAttachmentTypes.image) { - attachmentType = 'image'; - } else if (fileType == DefaultAttachmentTypes.video) { - attachmentType = 'video'; - } else if (fileType == DefaultAttachmentTypes.file) { - attachmentType = 'file'; - } - - if (camera) { - XFile? pickedFile; - if (fileType == DefaultAttachmentTypes.image) { - pickedFile = await _imagePicker.pickImage(source: ImageSource.camera); - } else if (fileType == DefaultAttachmentTypes.video) { - pickedFile = await _imagePicker.pickVideo(source: ImageSource.camera); - } - if (pickedFile != null) { - final bytes = await pickedFile.readAsBytes(); - file = AttachmentFile( - size: bytes.length, - path: pickedFile.path, - bytes: bytes, - ); - } - } else { - late FileType type; - if (fileType == DefaultAttachmentTypes.image) { - type = FileType.image; - } else if (fileType == DefaultAttachmentTypes.video) { - type = FileType.video; - } else if (fileType == DefaultAttachmentTypes.file) { - type = FileType.any; - } - final res = await FilePicker.platform.pickFiles( - type: type, - ); - if (res?.files.isNotEmpty == true) { - file = res!.files.single.toAttachmentFile; - } - } - - setState(() => _inputEnabled = true); - - if (file == null) return; - - final mimeType = file.name?.mimeType ?? file.path!.split('/').last.mimeType; - - final extraDataMap = {}; - - if (mimeType?.subtype != null) { - extraDataMap['mime_type'] = mimeType!.subtype.toLowerCase(); - } - - extraDataMap['file_size'] = file.size!; - - final attachment = Attachment( - file: file, - type: attachmentType, - uploadState: const UploadState.preparing(), - extraData: extraDataMap, - ); - - if (file.size! > widget.maxAttachmentSize) { - return _showErrorAlert( - context.translations.fileTooLargeError( - widget.maxAttachmentSize / (1024 * 1024), - ), - ); - } - - setState(() { - _addAttachments([ - attachment.copyWith( - file: file, - extraData: {...attachment.extraData} - ..update('file_size', ((_) => file!.size!)), - ), - ]); - }); - } - - Widget _buildIdleSendButton(BuildContext context) => Padding( - padding: const EdgeInsets.all(8), - child: StreamSvgIcon( - assetName: _getIdleSendIcon(), - color: _messageInputTheme.sendButtonIdleColor, - ), - ); - - Widget _buildSendButton(BuildContext context) => Padding( - padding: const EdgeInsets.all(8), - child: IconButton( - onPressed: sendMessage, - padding: EdgeInsets.zero, - splashRadius: 24, - constraints: const BoxConstraints.tightFor( - height: 24, - width: 24, - ), - icon: StreamSvgIcon( - assetName: _getSendIcon(), - color: _messageInputTheme.sendButtonColor, - ), - ), - ); - - String _getIdleSendIcon() { - if (_commandEnabled) { - return 'Icon_search.svg'; - } else { - return 'Icon_circle_right.svg'; - } - } - - String _getSendIcon() { - if (widget.editMessage != null) { - return 'Icon_circle_up.svg'; - } else if (_commandEnabled) { - return 'Icon_search.svg'; - } else { - return 'Icon_circle_up.svg'; - } - } - - /// Sends the current message - Future sendMessage() async { - var text = textEditingController.text.trim(); - if (text.isEmpty && _attachments.isEmpty) { - return; - } - - var shouldKeepFocus = widget.shouldKeepFocusAfterMessage; - - shouldKeepFocus ??= !_commandEnabled; - - if (_commandEnabled) { - text = '${'/${_chosenCommand!.name} '}$text'; - } - - final attachments = [..._attachments.values]; - - textEditingController.clear(); - _attachments.clear(); - widget.onQuotedMessageCleared?.call(); - - setState(() { - _commandEnabled = false; - }); - - Message message; - if (widget.editMessage != null) { - message = widget.editMessage!.copyWith( - text: text, - attachments: attachments, - mentionedUsers: - _mentionedUsers.where((u) => text.contains('@${u.name}')).toList(), - ); - } else { - message = (widget.initialMessage ?? Message()).copyWith( - parentId: widget.parentMessage?.id, - text: text, - attachments: attachments, - mentionedUsers: - _mentionedUsers.where((u) => text.contains('@${u.name}')).toList(), - showInChannel: widget.parentMessage != null ? _sendAsDm : null, - ); - } - - if (widget.quotedMessage != null) { - message = message.copyWith( - quotedMessageId: widget.quotedMessage!.id, - ); - } - - if (widget.preMessageSending != null) { - message = await widget.preMessageSending!(message); - } - - final streamChannel = StreamChannel.of(context); - final channel = streamChannel.channel; - if (!channel.state!.isUpToDate) { - await streamChannel.reloadChannel(); - } - - _mentionedUsers.clear(); - - message = _replaceUserNameWithId(message); - - try { - Future sendingFuture; - if (widget.editMessage == null || - widget.editMessage!.status == MessageSendingStatus.failed || - widget.editMessage!.status == MessageSendingStatus.sending) { - sendingFuture = channel.sendMessage(message); - } else { - sendingFuture = channel.updateMessage(message); - } - - if (shouldKeepFocus) { - FocusScope.of(context).requestFocus(_focusNode); - } else { - FocusScope.of(context).unfocus(); - } - - final resp = await sendingFuture; - if (resp.message?.type == 'error') { - _parseExistingMessage(message); - } - _startSlowMode(); - widget.onMessageSent?.call(resp.message); - } catch (e, stk) { - if (widget.onError != null) { - widget.onError?.call(e, stk); - } else { - rethrow; - } - } - } - - void _showErrorAlert(String description) { - showModalBottomSheet( - backgroundColor: _streamChatTheme.colorTheme.barsBg, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - ), - builder: (context) => Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox( - height: 26, - ), - StreamSvgIcon.error( - color: _streamChatTheme.colorTheme.accentError, - size: 24, - ), - const SizedBox( - height: 26, - ), - Text( - context.translations.somethingWentWrongError, - style: _streamChatTheme.textTheme.headlineBold, - ), - const SizedBox( - height: 7, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - description, - textAlign: TextAlign.center, - ), - ), - const SizedBox( - height: 36, - ), - Container( - color: - _streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.08), - height: 1, - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text( - context.translations.okLabel, - style: _streamChatTheme.textTheme.bodyBold.copyWith( - color: _streamChatTheme.colorTheme.accentPrimary, - ), - ), - ), - ], - ), - ], - ), - ); - } - - void _parseExistingMessage(Message message) { - final messageText = message.text; - if (messageText != null) textEditingController.text = messageText; - _addAttachments(message.attachments); - } - - @override - void dispose() { - textEditingController.dispose(); - _focusNode.removeListener(_focusNodeListener); - if (_isInternalFocusNode) _focusNode.dispose(); - _stopSlowMode(); - _onChangedDebounced.cancel(); - super.dispose(); - } - - bool _initialized = false; - - @override - void didChangeDependencies() { - _streamChatTheme = StreamChatTheme.of(context); - _messageInputTheme = StreamMessageInputTheme.of(context); - if (widget.editMessage == null) _startSlowMode(); - - if ((widget.editMessage != null || widget.initialMessage != null) && - !_initialized) { - FocusScope.of(context).requestFocus(_focusNode); - _initialized = true; - } - super.didChangeDependencies(); - } -} - -class _PickerWidget extends StatefulWidget { - const _PickerWidget({ - required this.filePickerIndex, - required this.containsFile, - required this.selectedMedias, - required this.onAddMoreFilesClick, - required this.onMediaSelected, - required this.streamChatTheme, - required this.mediaListViewController, - }); - - final int filePickerIndex; - final bool containsFile; - final List selectedMedias; - final void Function(DefaultAttachmentTypes) onAddMoreFilesClick; - final void Function(AssetEntity) onMediaSelected; - final StreamChatThemeData streamChatTheme; - final MediaListViewController mediaListViewController; - - @override - _PickerWidgetState createState() => _PickerWidgetState(); -} - -class _PickerWidgetState extends State<_PickerWidget> { - Future? requestPermission; - - @override - void initState() { - super.initState(); - requestPermission = PhotoManager.requestPermissionExtend(); - } - - @override - Widget build(BuildContext context) { - if (widget.filePickerIndex != 0) { - return const Offstage(); - } - return FutureBuilder( - future: requestPermission, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Offstage(); - } - - if ([PermissionState.authorized, PermissionState.limited] - .contains(snapshot.data)) { - if (widget.containsFile) { - return GestureDetector( - onTap: () { - widget.onAddMoreFilesClick(DefaultAttachmentTypes.file); - }, - child: Container( - constraints: const BoxConstraints.expand(), - color: widget.streamChatTheme.colorTheme.inputBg, - alignment: Alignment.center, - child: Text( - context.translations.addMoreFilesLabel, - style: TextStyle( - color: widget.streamChatTheme.colorTheme.accentPrimary, - fontWeight: FontWeight.bold, - ), - ), - ), - ); - } - - return StreamMediaListView( - controller: widget.mediaListViewController, - selectedIds: widget.selectedMedias, - onSelect: widget.onMediaSelected, - ); - } - - return InkWell( - onTap: () async { - PhotoManager.openSetting(); - }, - child: ColoredBox( - color: widget.streamChatTheme.colorTheme.inputBg, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SvgPicture.asset( - 'svgs/icon_picture_empty_state.svg', - package: 'stream_chat_flutter', - height: 140, - color: widget.streamChatTheme.colorTheme.disabled, - ), - Text( - context.translations.enablePhotoAndVideoAccessMessage, - style: widget.streamChatTheme.textTheme.body.copyWith( - color: widget.streamChatTheme.colorTheme.textLowEmphasis, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 6), - Center( - child: Text( - context.translations.allowGalleryAccessMessage, - style: widget.streamChatTheme.textTheme.bodyBold.copyWith( - color: widget.streamChatTheme.colorTheme.accentPrimary, - ), - ), - ), - ], - ), - ), - ); - }, - ); - } -} - -class _CountdownButton extends StatelessWidget { - const _CountdownButton({required this.count}); - - final int count; - - @override - Widget build(BuildContext context) => Padding( - padding: const EdgeInsets.all(8), - child: DecoratedBox( - decoration: BoxDecoration( - color: StreamChatTheme.of(context).colorTheme.disabled, - shape: BoxShape.circle, - ), - child: SizedBox( - height: 24, - width: 24, - child: Center( - child: Text('$count'), - ), - ), - ), - ); -} - -Message _replaceUserNameWithId(Message message) { - final mentionedUsers = message.mentionedUsers; - if (mentionedUsers.isEmpty) return message; - - var messageTextToSend = message.text; - if (messageTextToSend == null) return message; - - for (final user in mentionedUsers.toSet()) { - final userName = user.name; - messageTextToSend = messageTextToSend!.replaceAll( - '@$userName', - '@${user.id}', - ); - } - - return message.copyWith(text: messageTextToSend); -} diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_button.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_button.dart new file mode 100644 index 00000000..e559b58a --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_button.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template attachmentButton} +/// A button for adding attachments to a chat on mobile. +/// {@endtemplate} +class AttachmentButton extends StatelessWidget { + /// {@macro attachmentButton} + const AttachmentButton({ + super.key, + required this.color, + required this.onPressed, + }); + + /// The color of the button. + final Color color; + + /// The callback to perform when the button is tapped or clicked. + final VoidCallback onPressed; + + /// Returns a copy of this object with the given fields updated. + AttachmentButton copyWith({ + Key? key, + Color? color, + VoidCallback? onPressed, + }) { + return AttachmentButton( + key: key ?? this.key, + color: color ?? this.color, + onPressed: onPressed ?? this.onPressed, + ); + } + + @override + Widget build(BuildContext context) { + return IconButton( + icon: StreamSvgIcon.attach( + color: color, + ), + padding: EdgeInsets.zero, + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, + ), + splashRadius: 24, + onPressed: onPressed, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/options.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/options.dart new file mode 100644 index 00000000..298ab43e --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/options.dart @@ -0,0 +1,4 @@ +export 'stream_file_picker.dart'; +export 'stream_gallery_picker.dart'; +export 'stream_image_picker.dart'; +export 'stream_video_picker.dart'; diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_file_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_file_picker.dart new file mode 100644 index 00000000..153a8d02 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_file_picker.dart @@ -0,0 +1,115 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:photo_manager/photo_manager.dart'; +import 'package:stream_chat_flutter/src/attachment/handler/stream_attachment_handler.dart'; +import 'package:stream_chat_flutter/src/message_input/attachment_picker/stream_attachment_picker.dart'; +import 'package:stream_chat_flutter/src/misc/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/utils/utils.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// Widget used to pick files from the device +class StreamFilePicker extends StatelessWidget { + /// Creates a [StreamFilePicker] widget. + const StreamFilePicker({ + super.key, + required this.onFilePicked, + this.dialogTitle, + this.initialDirectory, + this.type = FileType.any, + this.allowedExtensions, + this.onFileLoading, + this.allowCompression = true, + this.withData = false, + this.withReadStream = false, + this.lockParentWindow = false, + }); + + /// Callback called when a file is picked. + final ValueSetter onFilePicked; + + /// Title of the file picker dialog. + final String? dialogTitle; + + /// Initial directory of the file picker dialog. + final String? initialDirectory; + + /// Type of the file to pick. + final FileType type; + + /// Allowed extensions of the file to pick. + final List? allowedExtensions; + + /// Callback called when the file picker is loading a file. + final Function(FilePickerStatus)? onFileLoading; + + /// Whether to allow compression of the file. + final bool allowCompression; + + /// Whether to include the file data in the [Attachment]. + final bool withData; + + /// Whether to include the file read stream in the [Attachment]. + final bool withReadStream; + + /// Whether to lock the parent window when the file picker is open. + final bool lockParentWindow; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + return OptionDrawer( + child: EndOfFrameCallbackWidget( + child: StreamSvgIcon.files( + size: 240, + color: theme.colorTheme.disabled, + ), + onEndOfFrame: (_) async { + final pickedFile = await runInPermissionRequestLock(() { + return StreamAttachmentHandler.instance.pickFile( + dialogTitle: dialogTitle, + initialDirectory: initialDirectory, + type: type, + allowedExtensions: allowedExtensions, + onFileLoading: onFileLoading, + allowCompression: allowCompression, + withData: withData, + withReadStream: withReadStream, + lockParentWindow: lockParentWindow, + ); + }); + + onFilePicked.call(pickedFile); + }, + errorBuilder: (context, error, stacktrace) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + StreamSvgIcon.files( + size: 240, + color: theme.colorTheme.disabled, + ), + Text( + context.translations.enablePhotoAndVideoAccessMessage, + style: theme.textTheme.body.copyWith( + color: theme.colorTheme.textLowEmphasis, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + TextButton( + onPressed: PhotoManager.openSetting, + child: Text( + context.translations.allowGalleryAccessMessage, + style: theme.textTheme.bodyBold.copyWith( + color: theme.colorTheme.accentPrimary, + ), + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_gallery_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_gallery_picker.dart new file mode 100644 index 00000000..a361f1ad --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_gallery_picker.dart @@ -0,0 +1,270 @@ +import 'dart:io'; +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:photo_manager/photo_manager.dart'; +import 'package:stream_chat_flutter/src/message_input/attachment_picker/stream_attachment_picker.dart'; +import 'package:stream_chat_flutter/src/misc/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/scroll_view/photo_gallery/stream_photo_gallery.dart'; +import 'package:stream_chat_flutter/src/scroll_view/photo_gallery/stream_photo_gallery_controller.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/utils/utils.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// Max image resolution which can be resized by the CDN. +// Taken from https://getstream.io/chat/docs/flutter-dart/file_uploads/?language=dart#image-resizing +const maxCDNImageResolution = 16800000; + +/// Widget used to pick media from the device gallery. +class StreamGalleryPicker extends StatefulWidget { + /// Creates a [StreamGalleryPicker] widget. + const StreamGalleryPicker({ + super.key, + this.limit = 50, + required this.selectedMediaItems, + required this.onMediaItemSelected, + this.mediaThumbnailSize = const ThumbnailSize(400, 400), + this.mediaThumbnailFormat = ThumbnailFormat.jpeg, + this.mediaThumbnailQuality = 100, + this.mediaThumbnailScale = 1, + }); + + /// Maximum number of media items that can be selected. + final int limit; + + /// List of selected media items. + final Iterable selectedMediaItems; + + /// Callback called when an media item is selected. + final ValueSetter onMediaItemSelected; + + /// Size of the attachment thumbnails. + /// + /// Defaults to (400, 400). + final ThumbnailSize mediaThumbnailSize; + + /// Format of the attachment thumbnails. + /// + /// Defaults to [ThumbnailFormat.jpeg]. + final ThumbnailFormat mediaThumbnailFormat; + + /// The quality value for the attachment thumbnails. + /// + /// Valid from 1 to 100. + /// Defaults to 100. + final int mediaThumbnailQuality; + + /// The scale to apply on the [attachmentThumbnailSize]. + final double mediaThumbnailScale; + + @override + State createState() => _StreamGalleryPickerState(); +} + +class _StreamGalleryPickerState extends State { + Future? requestPermission; + late StreamPhotoGalleryController _controller; + + @override + void initState() { + super.initState(); + _controller = StreamPhotoGalleryController(limit: widget.limit); + requestPermission = runInPermissionRequestLock( + PhotoManager.requestPermissionExtend, + ); + } + + @override + void didUpdateWidget(StreamGalleryPicker oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.limit != oldWidget.limit) { + _controller.dispose(); + _controller = StreamPhotoGalleryController(limit: widget.limit); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: requestPermission, + builder: (context, snapshot) { + if (!snapshot.hasData) return const SizedBox.shrink(); + + final theme = StreamChatTheme.of(context); + final textTheme = theme.textTheme; + final colorTheme = theme.colorTheme; + + // Available on both Android and iOS. + final isAuthorized = snapshot.data == PermissionState.authorized; + // Only available on iOS. + final isLimited = snapshot.data == PermissionState.limited; + + final isPermissionGranted = isAuthorized || isLimited; + + return OptionDrawer( + actions: [ + if (isLimited) + IconButton( + color: colorTheme.accentPrimary, + icon: const Icon(Icons.add_circle_outline_rounded), + onPressed: () async { + await PhotoManager.presentLimited(); + _controller.doInitialLoad(); + }, + ), + ], + child: Builder( + builder: (context) { + if (!isPermissionGranted) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + StreamSvgIcon.pictures( + size: 240, + color: colorTheme.disabled, + ), + Text( + context.translations.enablePhotoAndVideoAccessMessage, + style: textTheme.body.copyWith( + color: colorTheme.textLowEmphasis, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + TextButton( + onPressed: PhotoManager.openSetting, + child: Text( + context.translations.allowGalleryAccessMessage, + style: textTheme.bodyBold.copyWith( + color: colorTheme.accentPrimary, + ), + ), + ), + ], + ); + } + + return StreamPhotoGallery( + shrinkWrap: true, + controller: _controller, + onMediaTap: widget.onMediaItemSelected, + loadMoreTriggerIndex: 10, + padding: const EdgeInsets.all(2), + thumbnailSize: widget.mediaThumbnailSize, + thumbnailFormat: widget.mediaThumbnailFormat, + thumbnailQuality: widget.mediaThumbnailQuality, + thumbnailScale: widget.mediaThumbnailScale, + itemBuilder: (context, mediaItems, index, defaultWidget) { + final media = mediaItems[index]; + return defaultWidget.copyWith( + selected: widget.selectedMediaItems.contains(media.id), + ); + }, + ); + }, + ), + ); + }, + ); + } +} + +/// +extension StreamImagePickerX on StreamAttachmentPickerController { + /// + Future addAssetAttachment(AssetEntity asset) async { + final mediaFile = await asset.originFile; + + if (mediaFile == null) return; + + var cachedFile = mediaFile; + + final type = asset.type; + if (type == AssetType.image) { + // Resize image if it's resolution is greater than the + // [maxCDNImageResolution]. + final imageResolution = asset.width * asset.height; + if (imageResolution > maxCDNImageResolution) { + final aspect = imageResolution / maxCDNImageResolution; + final updatedSize = asset.size / (math.sqrt(aspect)); + final resizedImage = await asset.thumbnailDataWithSize( + ThumbnailSize( + updatedSize.width.floor(), + updatedSize.height.floor(), + ), + quality: 70, + ); + + final tempDir = await getTemporaryDirectory(); + cachedFile = await File( + '${tempDir.path}/${mediaFile.path.split('/').last}', + ).create().then((it) => it.writeAsBytes(resizedImage!)); + } + } + + final file = AttachmentFile( + path: cachedFile.path, + size: await cachedFile.length(), + bytes: cachedFile.readAsBytesSync(), + ); + + final extraDataMap = {}; + + final mimeType = file.mimeType?.mimeType; + + if (mimeType != null) { + extraDataMap['mime_type'] = mimeType; + } + + extraDataMap['file_size'] = file.size!; + + final attachment = Attachment( + id: asset.id, + file: file, + type: asset.type.toAttachmentType(), + extraData: extraDataMap, + ); + + return addAttachment(attachment); + } + + /// + Future removeAssetAttachment(AssetEntity asset) async { + if (asset.type == AssetType.image) { + final image = await asset.originFile; + if (image != null) { + final tempDir = await getTemporaryDirectory(); + final cachedFile = + File('${tempDir.path}/${image.path.split('/').last}'); + if (cachedFile.existsSync()) { + cachedFile.deleteSync(); + } + } + } + return removeAttachmentById(asset.id); + } +} + +/// +extension AssetTypeX on AssetType { + /// + String toAttachmentType() { + switch (this) { + case AssetType.image: + return 'image'; + case AssetType.video: + return 'video'; + case AssetType.audio: + return 'audio'; + case AssetType.other: + return 'file'; + } + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_image_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_image_picker.dart new file mode 100644 index 00000000..a79aa319 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_image_picker.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:photo_manager/photo_manager.dart'; +import 'package:stream_chat_flutter/src/attachment/handler/stream_attachment_handler.dart'; +import 'package:stream_chat_flutter/src/message_input/attachment_picker/stream_attachment_picker.dart'; +import 'package:stream_chat_flutter/src/misc/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/src/utils/helpers.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// Widget used to pick images from the device. +class StreamImagePicker extends StatelessWidget { + /// Creates a [StreamImagePicker] widget. + const StreamImagePicker({ + super.key, + required this.onImagePicked, + this.source = ImageSource.camera, + this.maxWidth, + this.maxHeight, + this.imageQuality, + this.preferredCameraDevice = CameraDevice.rear, + }); + + /// Callback called when an image is picked. + final ValueSetter onImagePicked; + + /// Source of the image to pick. + final ImageSource source; + + /// Maximum width of the image. + final double? maxWidth; + + /// Maximum height of the image. + final double? maxHeight; + + /// Quality of the image. + final int? imageQuality; + + /// Preferred camera device to use. + final CameraDevice preferredCameraDevice; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + return OptionDrawer( + child: EndOfFrameCallbackWidget( + child: StreamSvgIcon.camera( + size: 240, + color: theme.colorTheme.disabled, + ), + onEndOfFrame: (_) async { + final pickedImage = await runInPermissionRequestLock(() { + return StreamAttachmentHandler.instance.pickImage( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice, + ); + }); + + onImagePicked.call(pickedImage); + }, + errorBuilder: (context, error, stacktrace) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + StreamSvgIcon.camera( + size: 240, + color: theme.colorTheme.disabled, + ), + Text( + context.translations.enablePhotoAndVideoAccessMessage, + style: theme.textTheme.body.copyWith( + color: theme.colorTheme.textLowEmphasis, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + TextButton( + onPressed: PhotoManager.openSetting, + child: Text( + context.translations.allowGalleryAccessMessage, + style: theme.textTheme.bodyBold.copyWith( + color: theme.colorTheme.accentPrimary, + ), + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_video_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_video_picker.dart new file mode 100644 index 00000000..0fbd2faf --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_video_picker.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:photo_manager/photo_manager.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// Widget used to capture video using the device camera. +class StreamVideoPicker extends StatelessWidget { + /// Creates a [StreamVideoPicker] widget. + const StreamVideoPicker({ + super.key, + required this.onVideoPicked, + this.source = ImageSource.camera, + this.preferredCameraDevice = CameraDevice.rear, + this.maxDuration, + }); + + /// Callback called when a video is picked. + final ValueSetter onVideoPicked; + + /// Source of the video to pick. + final ImageSource source; + + /// Preferred camera device to use. + final CameraDevice preferredCameraDevice; + + /// Maximum duration of the video. + final Duration? maxDuration; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + return OptionDrawer( + child: EndOfFrameCallbackWidget( + child: StreamSvgIcon.record( + size: 240, + color: theme.colorTheme.disabled, + ), + onEndOfFrame: (_) async { + final pickedVideo = await runInPermissionRequestLock(() { + return StreamAttachmentHandler.instance.pickVideo( + source: source, + preferredCameraDevice: preferredCameraDevice, + maxDuration: maxDuration, + ); + }); + + onVideoPicked.call(pickedVideo); + }, + errorBuilder: (context, error, stacktrace) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + StreamSvgIcon.record( + size: 240, + color: theme.colorTheme.disabled, + ), + Text( + context.translations.enablePhotoAndVideoAccessMessage, + style: theme.textTheme.body.copyWith( + color: theme.colorTheme.textLowEmphasis, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + TextButton( + onPressed: PhotoManager.openSetting, + child: Text( + context.translations.allowGalleryAccessMessage, + style: theme.textTheme.bodyBold.copyWith( + color: theme.colorTheme.accentPrimary, + ), + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker.dart new file mode 100644 index 00000000..6ec909d0 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker.dart @@ -0,0 +1,821 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/message_input/attachment_picker/options/options.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// The default maximum size for media attachments. +const kDefaultMaxAttachmentSize = 100 * 1024 * 1024; // 100MB in Bytes + +/// The default maximum number of media attachments. +const kDefaultMaxAttachmentCount = 10; + +/// Controller class for [StreamAttachmentPicker]. +class StreamAttachmentPickerController extends ValueNotifier> { + /// Creates a new instance of [StreamAttachmentPickerController]. + StreamAttachmentPickerController({ + this.initialAttachments, + this.maxAttachmentSize = kDefaultMaxAttachmentSize, + this.maxAttachmentCount = kDefaultMaxAttachmentCount, + }) : assert( + (initialAttachments?.length ?? 0) <= maxAttachmentCount, + '''The initial attachments count must be less than or equal to maxAttachmentCount''', + ), + super(initialAttachments ?? const []); + + /// The max attachment size allowed in bytes. + final int maxAttachmentSize; + + /// The max attachment count allowed. + final int maxAttachmentCount; + + /// The initial attachments. + final List? initialAttachments; + + @override + set value(List newValue) { + if (newValue.length > maxAttachmentCount) { + throw ArgumentError( + 'The maximum number of attachments is $maxAttachmentCount.', + ); + } + super.value = newValue; + } + + Future _saveToCache(AttachmentFile file) async { + // Cache the attachment in a temporary file. + return StreamAttachmentHandler.instance.saveAttachmentFile( + attachmentFile: file, + ); + } + + Future _removeFromCache(AttachmentFile file) { + // Remove the cached attachment file. + return StreamAttachmentHandler.instance.deleteAttachmentFile( + attachmentFile: file, + ); + } + + /// Adds a new attachment to the message. + Future addAttachment(Attachment attachment) async { + assert(attachment.fileSize != null, ''); + if (attachment.fileSize! > maxAttachmentSize) { + throw ArgumentError( + 'The size of the attachment is ${attachment.fileSize} bytes, ' + 'but the maximum size allowed is $maxAttachmentSize bytes.', + ); + } + + final file = attachment.file; + final uploadState = attachment.uploadState; + + // No need to cache the attachment if it's already uploaded + // or we are on web. + if (file == null || uploadState.isSuccess || isWeb) { + value = [...value, attachment]; + return; + } + + // Cache the attachment in a temporary file. + final tempFilePath = await _saveToCache(file); + + value = [ + ...value, + attachment.copyWith( + file: file.copyWith( + path: tempFilePath, + ), + ), + ]; + } + + /// Removes the specified [attachment] from the message. + Future removeAttachment(Attachment attachment) async { + final file = attachment.file; + final uploadState = attachment.uploadState; + + if (file == null || uploadState.isSuccess || isWeb) { + value = [...value]..remove(attachment); + return; + } + + // Remove the cached attachment file. + await _removeFromCache(file); + + value = [...value]..remove(attachment); + } + + /// Remove the attachment with the given [attachmentId]. + void removeAttachmentById(String attachmentId) { + final attachment = value.firstWhereOrNull( + (attachment) => attachment.id == attachmentId, + ); + + if (attachment == null) return; + + removeAttachment(attachment); + } + + /// Clears all the attachments. + Future clear() async { + final attachments = [...value]; + for (final attachment in attachments) { + final file = attachment.file; + final uploadState = attachment.uploadState; + + if (file == null || uploadState.isSuccess || isWeb) continue; + + await _removeFromCache(file); + } + value = const []; + } + + /// Resets the controller to its initial state. + Future reset() async { + final attachments = [...value]; + for (final attachment in attachments) { + final file = attachment.file; + final uploadState = attachment.uploadState; + + if (file == null || uploadState.isSuccess || isWeb) continue; + + await _removeFromCache(file); + } + value = initialAttachments ?? const []; + } +} + +/// The possible picker types of the attachment picker. +enum AttachmentPickerType { + /// The attachment picker will only allow to pick images. + images, + + /// The attachment picker will only allow to pick videos. + videos, + + /// The attachment picker will only allow to pick audios. + audios, + + /// The attachment picker will only allow to pick files or documents. + files, +} + +/// Function signature for building the attachment picker option view. +typedef AttachmentPickerOptionViewBuilder = Widget Function( + BuildContext context, + StreamAttachmentPickerController controller, +); + +/// Model class for the attachment picker options. +class AttachmentPickerOption { + /// Creates a new instance of [AttachmentPickerOption]. + const AttachmentPickerOption({ + this.key, + required this.supportedTypes, + required this.icon, + this.title, + this.optionViewBuilder, + }); + + /// A key to identify the option. + final String? key; + + /// The icon of the option. + final Widget icon; + + /// The title of the option. + final String? title; + + /// The supported types of the option. + final Iterable supportedTypes; + + /// The option view builder. + final AttachmentPickerOptionViewBuilder? optionViewBuilder; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AttachmentPickerOption && + runtimeType == other.runtimeType && + key == other.key && + const IterableEquality().equals(supportedTypes, other.supportedTypes); + + @override + int get hashCode => + key.hashCode ^ const IterableEquality().hash(supportedTypes); +} + +/// The attachment picker option for the web or desktop platforms. +class WebOrDesktopAttachmentPickerOption extends AttachmentPickerOption { + /// Creates a new instance of [WebOrDesktopAttachmentPickerOption]. + WebOrDesktopAttachmentPickerOption({ + super.key, + required AttachmentPickerType type, + required super.icon, + required super.title, + }) : super(supportedTypes: [type]); + + /// Creates a new instance of [WebOrDesktopAttachmentPickerOption] from + /// [option]. + factory WebOrDesktopAttachmentPickerOption.fromAttachmentPickerOption( + AttachmentPickerOption option, + ) { + return WebOrDesktopAttachmentPickerOption( + key: option.key, + type: option.supportedTypes.first, + icon: option.icon, + title: option.title, + ); + } + + @override + String get title => super.title!; + + /// Type of the option. + AttachmentPickerType get type => supportedTypes.first; +} + +/// Helpful extensions for [StreamAttachmentPickerController]. +extension AttachmentPickerOptionTypeX on StreamAttachmentPickerController { + /// Returns the list of available attachment picker options. + Set get currentAttachmentPickerTypes { + final containsImage = value.any((it) => it.type == 'image'); + final containsVideo = value.any((it) => it.type == 'video'); + final containsAudio = value.any((it) => it.type == 'audio'); + final containsFile = value.any((it) => it.type == 'file'); + + return { + if (containsImage) AttachmentPickerType.images, + if (containsVideo) AttachmentPickerType.videos, + if (containsAudio) AttachmentPickerType.audios, + if (containsFile) AttachmentPickerType.files, + }; + } + + /// Returns the list of enabled picker types. + Set filterEnabledTypes({ + required Iterable options, + }) { + final availableTypes = currentAttachmentPickerTypes; + final enabledTypes = {}; + for (final option in options) { + final supportedTypes = option.supportedTypes; + if (availableTypes.any(supportedTypes.contains)) { + enabledTypes.addAll(supportedTypes); + } + } + return enabledTypes; + } + + /// Returns true if the [initialAttachments] are changed. + bool get isValueChanged { + final isEqual = UnorderedIterableEquality( + EqualityBy((Attachment attachment) => attachment.id), + ).equals(value, initialAttachments); + + return !isEqual; + } +} + +/// Function signature for the callback when the web or desktop attachment +/// picker option gets tapped. +typedef OnWebOrDesktopAttachmentPickerOptionTap = void Function( + BuildContext context, + StreamAttachmentPickerController controller, + WebOrDesktopAttachmentPickerOption option, +); + +/// Bottom sheet widget for the web or desktop version of the attachment picker. +class StreamWebOrDesktopAttachmentPickerBottomSheet extends StatelessWidget { + /// Creates a new instance of [StreamWebOrDesktopAttachmentPickerBottomSheet]. + const StreamWebOrDesktopAttachmentPickerBottomSheet({ + super.key, + required this.options, + required this.controller, + this.onOptionTap, + }); + + /// The list of options. + final Set options; + + /// The controller of the attachment picker. + final StreamAttachmentPickerController controller; + + /// The callback when the option gets tapped. + final OnWebOrDesktopAttachmentPickerOptionTap? onOptionTap; + + @override + Widget build(BuildContext context) { + final enabledTypes = controller.filterEnabledTypes(options: options); + return ListView( + shrinkWrap: true, + children: [ + ...options.map((option) { + VoidCallback? onOptionTap; + if (this.onOptionTap != null) { + onOptionTap = () { + this.onOptionTap?.call(context, controller, option); + }; + } + + final enabled = enabledTypes.isEmpty || + enabledTypes.any((it) => it == option.type); + + return ListTile( + enabled: enabled, + leading: option.icon, + title: Text(option.title), + onTap: onOptionTap, + ); + }), + ], + ); + } +} + +/// Bottom sheet widget for the mobile version of the attachment picker. +class StreamMobileAttachmentPickerBottomSheet extends StatefulWidget { + /// Creates a new instance of [StreamMobileAttachmentPickerBottomSheet]. + const StreamMobileAttachmentPickerBottomSheet({ + super.key, + required this.options, + required this.controller, + this.initialOption, + this.onSendAttachments, + }); + + /// The list of options. + final Set options; + + /// The initial option to be selected. + final AttachmentPickerOption? initialOption; + + /// The controller of the attachment picker. + final StreamAttachmentPickerController controller; + + /// The callback when the send button gets tapped. + final ValueSetter>? onSendAttachments; + + @override + State createState() => + _StreamMobileAttachmentPickerBottomSheetState(); +} + +class _StreamMobileAttachmentPickerBottomSheetState + extends State { + late AttachmentPickerOption _currentOption; + + @override + void initState() { + super.initState(); + if (widget.initialOption == null) { + final enabledTypes = widget.controller.filterEnabledTypes( + options: widget.options, + ); + if (enabledTypes.isNotEmpty) { + _currentOption = widget.options.firstWhere((it) { + return it.supportedTypes.contains(enabledTypes.first); + }); + } else { + _currentOption = widget.options.first; + } + } else { + _currentOption = widget.initialOption!; + } + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder>( + valueListenable: widget.controller, + builder: (context, attachments, _) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + _AttachmentPickerOptions( + controller: widget.controller, + options: widget.options, + currentOption: _currentOption, + onSendAttachment: widget.onSendAttachments, + onOptionSelected: (option) async { + setState(() => _currentOption = option); + }, + ), + Expanded( + child: _currentOption.optionViewBuilder + ?.call(context, widget.controller) ?? + const SizedBox.shrink(), + ), + ], + ); + }, + ); + } +} + +class _AttachmentPickerOptions extends StatelessWidget { + const _AttachmentPickerOptions({ + required this.options, + required this.currentOption, + required this.controller, + this.onOptionSelected, + this.onSendAttachment, + }); + + final Iterable options; + final AttachmentPickerOption currentOption; + final StreamAttachmentPickerController controller; + final ValueSetter? onOptionSelected; + final ValueSetter>? onSendAttachment; + + @override + Widget build(BuildContext context) { + final colorTheme = StreamChatTheme.of(context).colorTheme; + return ValueListenableBuilder>( + valueListenable: controller, + builder: (context, attachments, __) { + final enabledTypes = controller.filterEnabledTypes(options: options); + return Row( + children: [ + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + ...options.map( + (option) { + final supportedTypes = option.supportedTypes; + + final isSelected = option == currentOption; + final isEnabled = enabledTypes.isEmpty || + enabledTypes.any(supportedTypes.contains); + + final color = isSelected + ? colorTheme.accentPrimary + : colorTheme.textLowEmphasis; + + final onPressed = + isEnabled ? () => onOptionSelected!(option) : null; + + return IconButton( + color: color, + disabledColor: colorTheme.disabled, + icon: option.icon, + onPressed: onPressed, + ); + }, + ), + ], + ), + ), + ), + Builder( + builder: (context) { + final isEnabled = + onSendAttachment != null && controller.isValueChanged; + + final onPressed = isEnabled + ? () { + onSendAttachment!(attachments); + } + : null; + + return IconButton( + iconSize: 22, + color: colorTheme.accentPrimary, + disabledColor: colorTheme.disabled, + icon: StreamSvgIcon.emptyCircleLeft().toIconThemeSvgIcon(), + onPressed: onPressed, + ); + }, + ), + ], + ); + }, + ); + } +} + +/// Signature used by [EndOfFrameCallbackWidget.errorBuilder] to create a +/// replacement widget to render. +typedef EndOfFrameCallbackErrorWidgetBuilder = Widget Function( + BuildContext context, + Object error, + StackTrace? stackTrace, +); + +/// Function signature for a callback that is called when the end of the frame +/// is reached. +typedef EndOfFrameCallback = FutureOr Function(BuildContext context); + +/// A widget that calls the given [callback] when the end of the frame is +/// reached. +class EndOfFrameCallbackWidget extends StatefulWidget { + /// Creates a new instance of [EndOfFrameCallbackWidget]. + const EndOfFrameCallbackWidget({ + super.key, + required this.onEndOfFrame, + this.child, + this.errorBuilder, + }); + + /// The widget below this widget in the tree. + final Widget? child; + + /// The callback that is called when the end of the frame is reached.x + final EndOfFrameCallback onEndOfFrame; + + /// The callback that will be called if the [onEndOfFrame] callback throws an + /// error. + final EndOfFrameCallbackErrorWidgetBuilder? errorBuilder; + + @override + State createState() => + _EndOfFrameCallbackWidgetState(); +} + +class _EndOfFrameCallbackWidgetState extends State { + Object? _error; + StackTrace? _stackTrace; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.endOfFrame.then((_) async { + if (mounted) { + try { + await widget.onEndOfFrame(context); + } catch (error, stackTrace) { + setState(() { + _error = error; + _stackTrace = stackTrace; + }); + } + } + }); + } + + @override + Widget build(BuildContext context) { + final error = _error; + final stackTrace = _stackTrace; + + if (error != null) { + final errorBuilder = widget.errorBuilder; + if (errorBuilder != null) { + return errorBuilder(context, error, stackTrace); + } + return const Text('An error occurred'); + } + + // Reset the error and stack trace so that we don't keep showing the same + // error over and over. + _error = null; + _stackTrace = null; + + return widget.child ?? const SizedBox.shrink(); + } +} + +const _kDefaultOptionDrawerShape = RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), +); + +/// A widget that will be shown in the attachment picker. +/// It can be used to show a custom view for each attachment picker option. +class OptionDrawer extends StatelessWidget { + /// Creates a widget that will be shown in the attachment picker. + const OptionDrawer({ + super.key, + required this.child, + this.color, + this.elevation = 2, + this.margin = EdgeInsets.zero, + this.clipBehavior = Clip.hardEdge, + this.shape = _kDefaultOptionDrawerShape, + this.title, + this.actions = const [], + }); + + /// The widget below this widget in the tree. + final Widget child; + + /// The background color of the options card. + /// + /// Defaults to [StreamColorTheme.barsBg]. + final Color? color; + + /// The elevation of the options card. + /// + /// The default value is 2. + final double elevation; + + /// The margin of the options card. + /// + /// The default value is [EdgeInsets.zero]. + final EdgeInsetsGeometry margin; + + /// The clip behavior of the options card. + /// + /// The default value is [Clip.hardEdge]. + final Clip clipBehavior; + + /// The shape of the options card. + final ShapeBorder shape; + + /// The title of the options card. + final Widget? title; + + /// The actions available for the options card. + final List actions; + + @override + Widget build(BuildContext context) { + final colorTheme = StreamChatTheme.of(context).colorTheme; + + var height = 20.0; + if (title != null || actions.isNotEmpty) { + height = 40.0; + } + + final leading = title ?? const SizedBox.shrink(); + + Widget trailing; + if (actions.isNotEmpty) { + trailing = Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: actions, + ); + } else { + trailing = const SizedBox.shrink(); + } + + return Card( + elevation: elevation, + color: color ?? colorTheme.barsBg, + margin: margin, + shape: shape, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: height, + child: Row( + children: [ + Expanded(child: leading), + Container( + height: 4, + width: 40, + decoration: BoxDecoration( + color: colorTheme.disabled, + borderRadius: BorderRadius.circular(6), + ), + ), + Expanded(child: trailing), + ], + ), + ), + Expanded(child: child), + ], + ), + ); + } +} + +/// Returns the mobile version of the attachment picker. +Widget mobileAttachmentPickerBuilder({ + required BuildContext context, + required StreamAttachmentPickerController controller, + Iterable? customOptions, + ThumbnailSize attachmentThumbnailSize = const ThumbnailSize(400, 400), + ThumbnailFormat attachmentThumbnailFormat = ThumbnailFormat.jpeg, + int attachmentThumbnailQuality = 100, + double attachmentThumbnailScale = 1, +}) { + return StreamMobileAttachmentPickerBottomSheet( + controller: controller, + onSendAttachments: Navigator.of(context).pop, + options: { + if (customOptions != null) ...customOptions, + AttachmentPickerOption( + key: 'gallery-picker', + icon: StreamSvgIcon.pictures(size: 36).toIconThemeSvgIcon(), + supportedTypes: [ + AttachmentPickerType.images, + AttachmentPickerType.videos, + ], + optionViewBuilder: (context, controller) { + final selectedIds = controller.value.map((it) => it.id); + return StreamGalleryPicker( + selectedMediaItems: selectedIds, + mediaThumbnailSize: attachmentThumbnailSize, + mediaThumbnailFormat: attachmentThumbnailFormat, + mediaThumbnailQuality: attachmentThumbnailQuality, + mediaThumbnailScale: attachmentThumbnailScale, + onMediaItemSelected: (media) async { + if (selectedIds.contains(media.id)) { + return controller.removeAssetAttachment(media); + } + return controller.addAssetAttachment(media); + }, + ); + }, + ), + AttachmentPickerOption( + key: 'file-picker', + icon: StreamSvgIcon.files(size: 36).toIconThemeSvgIcon(), + supportedTypes: [AttachmentPickerType.files], + optionViewBuilder: (context, controller) { + return StreamFilePicker( + onFilePicked: (file) async { + if (file != null) await controller.addAttachment(file); + return Navigator.pop(context, controller.value); + }, + ); + }, + ), + AttachmentPickerOption( + key: 'image-picker', + icon: StreamSvgIcon.camera(size: 36).toIconThemeSvgIcon(), + supportedTypes: [AttachmentPickerType.images], + optionViewBuilder: (context, controller) { + return StreamImagePicker( + onImagePicked: (image) async { + if (image != null) { + await controller.addAttachment(image); + } + return Navigator.pop(context, controller.value); + }, + ); + }, + ), + AttachmentPickerOption( + key: 'video-picker', + icon: StreamSvgIcon.record(size: 36).toIconThemeSvgIcon(), + supportedTypes: [AttachmentPickerType.videos], + optionViewBuilder: (context, controller) { + return StreamVideoPicker( + onVideoPicked: (video) async { + if (video != null) { + await controller.addAttachment(video); + } + return Navigator.pop(context, controller.value); + }, + ); + }, + ), + }, + ); +} + +/// Returns the web or desktop version of the attachment picker. +Widget webOrDesktopAttachmentPickerBuilder({ + required BuildContext context, + required StreamAttachmentPickerController controller, + Iterable? customOptions, + ThumbnailSize attachmentThumbnailSize = const ThumbnailSize(400, 400), + ThumbnailFormat attachmentThumbnailFormat = ThumbnailFormat.jpeg, + int attachmentThumbnailQuality = 100, + double attachmentThumbnailScale = 1, +}) { + return StreamWebOrDesktopAttachmentPickerBottomSheet( + controller: controller, + options: { + if (customOptions != null) ...customOptions, + WebOrDesktopAttachmentPickerOption( + key: 'image-picker', + type: AttachmentPickerType.images, + icon: StreamSvgIcon.pictures(size: 36).toIconThemeSvgIcon(), + title: 'Upload a photo', + ), + WebOrDesktopAttachmentPickerOption( + key: 'video-picker', + type: AttachmentPickerType.videos, + icon: StreamSvgIcon.record(size: 36).toIconThemeSvgIcon(), + title: 'Upload a video', + ), + WebOrDesktopAttachmentPickerOption( + key: 'file-picker', + type: AttachmentPickerType.files, + icon: StreamSvgIcon.files(size: 36).toIconThemeSvgIcon(), + title: 'Upload a file', + ), + }, + onOptionTap: (context, controller, option) async { + final attachment = await StreamAttachmentHandler.instance.pickFile( + type: option.type.fileType, + ); + if (attachment != null) { + await controller.addAttachment(attachment); + } + return Navigator.pop(context, controller.value); + }, + ); +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_bottom_sheet.dart new file mode 100644 index 00000000..3113feb1 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_bottom_sheet.dart @@ -0,0 +1,242 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/platform_widget_builder/src/platform_widget.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// Shows a modal material design bottom sheet. +/// +/// A modal bottom sheet is an alternative to a menu or a dialog and prevents +/// the user from interacting with the rest of the app. +/// +/// A closely related widget is a persistent bottom sheet, which shows +/// information that supplements the primary content of the app without +/// preventing the use from interacting with the app. Persistent bottom sheets +/// can be created and displayed with the [showBottomSheet] function or the +/// [ScaffoldState.showBottomSheet] method. +/// +/// The `context` argument is used to look up the [Navigator] and [Theme] for +/// the bottom sheet. It is only used when the method is called. Its +/// corresponding widget can be safely removed from the tree before the bottom +/// sheet is closed. +/// +/// The `isScrollControlled` parameter specifies whether this is a route for +/// a bottom sheet that will utilize [DraggableScrollableSheet]. If you wish +/// to have a bottom sheet that has a scrollable child such as a [ListView] or +/// a [GridView] and have the bottom sheet be draggable, you should set this +/// parameter to true. +/// +/// The `useRootNavigator` parameter ensures that the root navigator is used to +/// display the [BottomSheet] when set to `true`. This is useful in the case +/// that a modal [BottomSheet] needs to be displayed above all other content +/// but the caller is inside another [Navigator]. +/// +/// The [isDismissible] parameter specifies whether the bottom sheet will be +/// dismissed when user taps on the scrim. +/// +/// The [enableDrag] parameter specifies whether the bottom sheet can be +/// dragged up and down and dismissed by swiping downwards. +/// +/// The optional [backgroundColor], [elevation], [shape], [clipBehavior], +/// [constraints] and [transitionAnimationController] +/// parameters can be passed in to customize the appearance and behavior of +/// modal bottom sheets (see the documentation for these on [BottomSheet] +/// for more details). +/// +/// The [transitionAnimationController] controls the bottom sheet's entrance and +/// exit animations if provided. +/// +/// The optional `routeSettings` parameter sets the [RouteSettings] +/// of the modal bottom sheet sheet. +/// This is particularly useful in the case that a user wants to observe +/// [PopupRoute]s within a [NavigatorObserver]. +/// +/// Returns a `Future` that resolves to the value (if any) that was passed to +/// [Navigator.pop] when the modal bottom sheet was closed. +/// +/// See also: +/// +/// * [BottomSheet], which becomes the parent of the widget returned by the +/// function passed as the `builder` argument to [showModalBottomSheet]. +/// * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing +/// non-modal bottom sheets. +/// * [DraggableScrollableSheet], which allows you to create a bottom sheet +/// that grows and then becomes scrollable once it reaches its maximum size. +/// * +Future showStreamAttachmentPickerModalBottomSheet({ + required BuildContext context, + Iterable? customOptions, + List? initialAttachments, + StreamAttachmentPickerController? controller, + Color? backgroundColor, + double? elevation, + BoxConstraints? constraints, + Color? barrierColor, + bool isScrollControlled = false, + bool useRootNavigator = false, + bool isDismissible = true, + bool enableDrag = true, + RouteSettings? routeSettings, + AnimationController? transitionAnimationController, + Clip? clipBehavior = Clip.hardEdge, + ShapeBorder? shape, + ThumbnailSize attachmentThumbnailSize = const ThumbnailSize(400, 400), + ThumbnailFormat attachmentThumbnailFormat = ThumbnailFormat.jpeg, + int attachmentThumbnailQuality = 100, + double attachmentThumbnailScale = 1, +}) { + final colorTheme = StreamChatTheme.of(context).colorTheme; + final color = backgroundColor ?? colorTheme.inputBg; + + return showModalBottomSheet( + context: context, + backgroundColor: color, + elevation: elevation, + shape: shape, + clipBehavior: clipBehavior, + constraints: constraints, + barrierColor: barrierColor, + isScrollControlled: isScrollControlled, + useRootNavigator: useRootNavigator, + isDismissible: isDismissible, + enableDrag: enableDrag, + routeSettings: routeSettings, + transitionAnimationController: transitionAnimationController, + builder: (BuildContext context) { + return StreamPlatformAttachmentPickerBottomSheetBuilder( + controller: controller, + initialAttachments: initialAttachments, + builder: (context, controller, child) { + return PlatformWidget( + web: (context) { + return webOrDesktopAttachmentPickerBuilder.call( + context: context, + controller: controller, + customOptions: customOptions?.map( + WebOrDesktopAttachmentPickerOption.fromAttachmentPickerOption, + ), + attachmentThumbnailSize: attachmentThumbnailSize, + attachmentThumbnailFormat: attachmentThumbnailFormat, + attachmentThumbnailQuality: attachmentThumbnailQuality, + attachmentThumbnailScale: attachmentThumbnailScale, + ); + }, + mobile: (context) { + return mobileAttachmentPickerBuilder.call( + context: context, + controller: controller, + customOptions: customOptions, + attachmentThumbnailSize: attachmentThumbnailSize, + attachmentThumbnailFormat: attachmentThumbnailFormat, + attachmentThumbnailQuality: attachmentThumbnailQuality, + attachmentThumbnailScale: attachmentThumbnailScale, + ); + }, + desktop: (context) { + return webOrDesktopAttachmentPickerBuilder.call( + context: context, + controller: controller, + customOptions: customOptions?.map( + WebOrDesktopAttachmentPickerOption.fromAttachmentPickerOption, + ), + attachmentThumbnailSize: attachmentThumbnailSize, + attachmentThumbnailFormat: attachmentThumbnailFormat, + attachmentThumbnailQuality: attachmentThumbnailQuality, + attachmentThumbnailScale: attachmentThumbnailScale, + ); + }, + ); + }, + ); + }, + ); +} + +/// Builds the attachment picker bottom sheet. +class StreamPlatformAttachmentPickerBottomSheetBuilder extends StatefulWidget { + /// Creates a new instance of the widget. + const StreamPlatformAttachmentPickerBottomSheetBuilder({ + super.key, + this.customOptions, + this.initialAttachments, + this.child, + this.controller, + required this.builder, + }); + + /// The child widget. + final Widget? child; + + /// Builder for the attachment picker bottom sheet. + final Widget Function( + BuildContext context, + StreamAttachmentPickerController controller, + Widget? child, + ) builder; + + /// The custom options to be displayed in the attachment picker. + final List? customOptions; + + /// The initial attachments. + final List? initialAttachments; + + /// The controller. + final StreamAttachmentPickerController? controller; + + @override + State createState() => + _StreamPlatformAttachmentPickerBottomSheetBuilderState(); +} + +class _StreamPlatformAttachmentPickerBottomSheetBuilderState + extends State { + late StreamAttachmentPickerController _controller; + + @override + void initState() { + super.initState(); + _controller = widget.controller ?? + StreamAttachmentPickerController( + initialAttachments: widget.initialAttachments, + ); + } + + // Handle a potential change in StreamAttachmentPickerController by properly + // disposing of the old one and setting up the new one, if needed. + void _updateTextEditingController( + StreamAttachmentPickerController? old, + StreamAttachmentPickerController? current, + ) { + if ((old == null && current == null) || old == current) return; + if (old == null) { + _controller.dispose(); + _controller = current!; + } else if (current == null) { + _controller = StreamAttachmentPickerController(); + } else { + _controller = current; + } + } + + @override + void didUpdateWidget( + StreamPlatformAttachmentPickerBottomSheetBuilder oldWidget, + ) { + super.didUpdateWidget(oldWidget); + _updateTextEditingController( + oldWidget.controller, + widget.controller, + ); + } + + @override + void dispose() { + if (widget.controller == null) _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return widget.builder(context, _controller, widget.child); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/clear_input_item_button.dart b/packages/stream_chat_flutter/lib/src/message_input/clear_input_item_button.dart new file mode 100644 index 00000000..414b7831 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/clear_input_item_button.dart @@ -0,0 +1,46 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template clearInputItemButton} +/// Used to clear items from the [MessageInput] field, such as attachments +/// or message quotes. +/// {@endtemplate} +class ClearInputItemButton extends StatelessWidget { + /// {@macro clearInputItemButton} + const ClearInputItemButton({ + super.key, + required this.onTap, + }); + + /// The callback to be performed when the button is tapped or clicked. + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + final _streamChatTheme = StreamChatTheme.of(context); + return SizedBox( + height: 20, + width: 20, + child: RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + highlightElevation: 0, + focusElevation: 0, + hoverElevation: 0, + onPressed: onTap, + fillColor: + _streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.5), + child: Center( + child: StreamSvgIcon.close( + size: 24, + color: _streamChatTheme.colorTheme.barsBg, + ), + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/command_button.dart b/packages/stream_chat_flutter/lib/src/message_input/command_button.dart new file mode 100644 index 00000000..218cdf81 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/command_button.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/misc/stream_svg_icon.dart'; + +/// {@template commandButton} +/// The button that allows a user to use commands in a chat. +/// {@endtemplate} +class CommandButton extends StatelessWidget { + /// {@macro commandButton} + const CommandButton({ + super.key, + required this.color, + required this.onPressed, + }); + + /// The color of the button. + final Color color; + + /// The action to perform when the button is pressed or clicked. + final VoidCallback onPressed; + + /// Returns a copy of this object with the given fields updated. + CommandButton copyWith({ + Key? key, + Color? color, + VoidCallback? onPressed, + }) { + return CommandButton( + key: key ?? this.key, + color: color ?? this.color, + onPressed: onPressed ?? this.onPressed, + ); + } + + @override + Widget build(BuildContext context) { + return IconButton( + icon: StreamSvgIcon.lightning( + color: color, + ), + padding: EdgeInsets.zero, + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, + ), + splashRadius: 24, + onPressed: onPressed, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart b/packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart new file mode 100644 index 00000000..2b832a0d --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// Button for showing visual component of slow mode. +class StreamCountdownButton extends StatelessWidget { + /// Constructor for creating [StreamCountdownButton]. + const StreamCountdownButton({ + super.key, + required this.count, + }); + + /// The amount of time remaining until the user can send a message again. + /// Measured in seconds. + final int count; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8), + child: DecoratedBox( + decoration: BoxDecoration( + color: StreamChatTheme.of(context).colorTheme.disabled, + shape: BoxShape.circle, + ), + child: SizedBox( + height: 24, + width: 24, + child: Center( + child: Text('$count'), + ), + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/dm_checkbox.dart b/packages/stream_chat_flutter/lib/src/message_input/dm_checkbox.dart new file mode 100644 index 00000000..8de842e5 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/dm_checkbox.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template dmCheckbox} +/// Prompts the user to send a reply to a message thread as a DM. +/// {@endtemplate} +class DmCheckbox extends StatelessWidget { + /// {@macro dmCheckbox} + const DmCheckbox({ + super.key, + required this.foregroundDecoration, + required this.color, + required this.onTap, + required this.crossFadeState, + }); + + /// The decoration to use for the button's foreground. + final BoxDecoration foregroundDecoration; + + /// The color to use for the button. + final Color color; + + /// The action to perform when the button is tapped or clicked. + final VoidCallback onTap; + + /// The [CrossFadeState] of the animation. + final CrossFadeState crossFadeState; + + @override + Widget build(BuildContext context) { + final _streamChatTheme = StreamChatTheme.of(context); + return Row( + children: [ + Container( + height: 16, + width: 16, + foregroundDecoration: foregroundDecoration, + child: Center( + child: Material( + borderRadius: BorderRadius.circular(3), + color: color, + child: InkWell( + onTap: onTap, + child: AnimatedCrossFade( + duration: const Duration(milliseconds: 300), + reverseDuration: const Duration(milliseconds: 300), + crossFadeState: crossFadeState, + firstChild: StreamSvgIcon.check( + size: 16, + color: _streamChatTheme.colorTheme.barsBg, + ), + secondChild: const SizedBox( + height: 16, + width: 16, + ), + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + context.translations.alsoSendAsDirectMessageLabel, + style: _streamChatTheme.textTheme.footnote.copyWith( + color: + _streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.5), + ), + ), + ), + ], + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/enums.dart b/packages/stream_chat_flutter/lib/src/message_input/enums.dart new file mode 100644 index 00000000..5b34b7f1 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/enums.dart @@ -0,0 +1,35 @@ +/// Location for actions on the [StreamMessageInput]. +enum ActionsLocation { + /// Align to left + left, + + /// Align to right + right, + + /// Align to left but inside the [TextField] + leftInside, + + /// Align to right but inside the [TextField] + rightInside, +} + +/// Default attachments for widget. +enum DefaultAttachmentTypes { + /// Image Attachment + image, + + /// Video Attachment + video, + + /// File Attachment + file, +} + +/// Available locations for the `sendMessage` button relative to the textField. +enum SendButtonLocation { + /// inside the textField + inside, + + /// outside the textField + outside, +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart b/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart new file mode 100644 index 00000000..0546b218 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart @@ -0,0 +1,392 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/platform_widget_builder/platform_widget_builder.dart'; +import 'package:stream_chat_flutter/src/message_input/clear_input_item_button.dart'; +import 'package:stream_chat_flutter/src/utils/utils.dart'; +import 'package:stream_chat_flutter/src/video/video_thumbnail_image.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:video_player/video_player.dart'; + +/// {@template streamQuotedMessage} +/// Widget for the quoted message. +/// {@endtemplate} +class StreamQuotedMessageWidget extends StatelessWidget { + /// {@macro streamQuotedMessage} + const StreamQuotedMessageWidget({ + super.key, + required this.message, + required this.messageTheme, + this.reverse = false, + this.showBorder = false, + this.textLimit = 170, + this.attachmentThumbnailBuilders, + this.padding = const EdgeInsets.all(8), + this.onTap, + this.onQuotedMessageClear, + this.composing = true, + }); + + /// The message + final Message message; + + /// The message theme + final StreamMessageThemeData messageTheme; + + /// If true the widget will be mirrored + final bool reverse; + + /// If true the message will show a grey border + final bool showBorder; + + /// limit of the text message shown + final int textLimit; + + /// Map that defines a thumbnail builder for an attachment type + final Map? + attachmentThumbnailBuilders; + + /// Padding around the widget + final EdgeInsetsGeometry padding; + + /// Callback for tap on widget + final GestureTapCallback? onTap; + + /// Callback for clearing quoted messages. + final VoidCallback? onQuotedMessageClear; + + /// True if the message is being composed + final bool composing; + + @override + Widget build(BuildContext context) { + final children = [ + Flexible( + child: _QuotedMessage( + message: message, + textLimit: textLimit, + composing: composing, + onQuotedMessageClear: onQuotedMessageClear, + messageTheme: messageTheme, + showBorder: showBorder, + reverse: reverse, + attachmentThumbnailBuilders: attachmentThumbnailBuilders, + ), + ), + const SizedBox(width: 8), + if (message.user != null) + StreamUserAvatar( + user: message.user!, + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, + ), + showOnlineStatus: false, + ), + ]; + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onTap, + child: Padding( + padding: padding, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: reverse ? children.reversed.toList() : children, + ), + ), + ), + ); + } +} + +class _QuotedMessage extends StatelessWidget { + const _QuotedMessage({ + required this.message, + required this.textLimit, + required this.composing, + required this.onQuotedMessageClear, + required this.messageTheme, + required this.showBorder, + required this.reverse, + this.attachmentThumbnailBuilders, + }); + + final Message message; + final int textLimit; + final bool composing; + final VoidCallback? onQuotedMessageClear; + final StreamMessageThemeData messageTheme; + final bool showBorder; + final bool reverse; + + /// Map that defines a thumbnail builder for an attachment type + final Map? + attachmentThumbnailBuilders; + + bool get _hasAttachments => message.attachments.isNotEmpty; + + bool get _containsText => message.text?.isNotEmpty == true; + + bool get _containsLinkAttachment => + message.attachments.any((element) => element.titleLink != null); + + bool get _isGiphy => + message.attachments.any((element) => element.type == 'giphy'); + + @override + Widget build(BuildContext context) { + final isOnlyEmoji = message.text!.isOnlyEmoji; + var msg = _hasAttachments && !_containsText + ? message.copyWith(text: message.attachments.last.title ?? '') + : message; + if (msg.text!.length > textLimit) { + msg = msg.copyWith(text: '${msg.text!.substring(0, textLimit - 3)}...'); + } + + final children = [ + if (composing) + PlatformWidgetBuilder( + web: (context, child) => child, + desktop: (context, child) => child, + child: ClearInputItemButton( + onTap: onQuotedMessageClear, + ), + ), + if (_hasAttachments) + _ParseAttachments( + message: message, + messageTheme: messageTheme, + attachmentThumbnailBuilders: attachmentThumbnailBuilders, + ), + if (msg.text!.isNotEmpty && !_isGiphy) + Flexible( + child: StreamMessageText( + message: msg, + messageTheme: isOnlyEmoji && _containsText + ? messageTheme.copyWith( + messageTextStyle: messageTheme.messageTextStyle?.copyWith( + fontSize: 32, + ), + ) + : messageTheme.copyWith( + messageTextStyle: messageTheme.messageTextStyle?.copyWith( + fontSize: 12, + ), + ), + ), + ), + ].insertBetween(const SizedBox(width: 8)); + + return Container( + decoration: BoxDecoration( + color: _getBackgroundColor(context), + border: showBorder + ? Border.all( + color: StreamChatTheme.of(context).colorTheme.disabled, + ) + : null, + borderRadius: BorderRadius.only( + topRight: const Radius.circular(12), + topLeft: const Radius.circular(12), + bottomRight: reverse ? const Radius.circular(12) : Radius.zero, + bottomLeft: reverse ? Radius.zero : const Radius.circular(12), + ), + ), + padding: const EdgeInsets.all(8), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: + reverse ? MainAxisAlignment.end : MainAxisAlignment.start, + children: reverse ? children.reversed.toList() : children, + ), + ); + } + + Color? _getBackgroundColor(BuildContext context) { + if (_containsLinkAttachment) { + return messageTheme.linkBackgroundColor; + } + return messageTheme.messageBackgroundColor; + } +} + +class _ParseAttachments extends StatelessWidget { + const _ParseAttachments({ + required this.message, + required this.messageTheme, + this.attachmentThumbnailBuilders, + }); + + final Message message; + final StreamMessageThemeData messageTheme; + final Map? + attachmentThumbnailBuilders; + + bool get _containsLinkAttachment => + message.attachments.any((element) => element.titleLink != null); + + @override + Widget build(BuildContext context) { + Widget child; + Attachment attachment; + if (_containsLinkAttachment) { + attachment = message.attachments.firstWhere( + (element) => element.ogScrapeUrl != null || element.titleLink != null, + ); + child = _UrlAttachment(attachment: attachment); + } else { + QuotedMessageAttachmentThumbnailBuilder? attachmentBuilder; + attachment = message.attachments.last; + if (attachmentThumbnailBuilders?.containsKey(attachment.type) == true) { + attachmentBuilder = attachmentThumbnailBuilders![attachment.type]; + } + attachmentBuilder = _defaultAttachmentBuilder[attachment.type]; + if (attachmentBuilder == null) { + child = const Offstage(); + } else { + child = attachmentBuilder(context, attachment); + } + } + child = AbsorbPointer(child: child); + return Material( + clipBehavior: Clip.hardEdge, + type: MaterialType.transparency, + shape: attachment.type == 'file' + ? null + : RoundedRectangleBorder( + side: const BorderSide(width: 0, color: Colors.transparent), + borderRadius: BorderRadius.circular(8), + ), + child: child, + ); + } + + Map + get _defaultAttachmentBuilder { + return { + 'image': (_, attachment) { + return StreamImageAttachment( + attachment: attachment, + message: message, + messageTheme: messageTheme, + constraints: BoxConstraints.loose(const Size(32, 32)), + ); + }, + 'video': (_, attachment) { + return StreamVideoThumbnailImage( + key: ValueKey(attachment.assetUrl), + video: attachment.file?.path ?? attachment.assetUrl!, + constraints: BoxConstraints.loose(const Size(32, 32)), + fit: BoxFit.cover, + errorBuilder: (_, __) => AttachmentError( + constraints: BoxConstraints.loose(const Size(32, 32)), + ), + ); + }, + 'giphy': (_, attachment) { + const size = Size(32, 32); + return CachedNetworkImage( + height: size.height, + width: size.width, + placeholder: (_, __) { + return SizedBox( + width: size.width, + height: size.height, + child: const Center( + child: CircularProgressIndicator(), + ), + ); + }, + imageUrl: attachment.thumbUrl ?? + attachment.imageUrl ?? + attachment.assetUrl!, + errorWidget: (context, url, error) => + AttachmentError(constraints: BoxConstraints.loose(size)), + fit: BoxFit.cover, + ); + }, + 'file': (_, attachment) { + return SizedBox( + height: 32, + width: 32, + child: getFileTypeImage( + attachment.extraData['mime_type'] as String?, + ), + ); + }, + }; + } +} + +class _UrlAttachment extends StatelessWidget { + const _UrlAttachment({ + required this.attachment, + }); + + final Attachment attachment; + + @override + Widget build(BuildContext context) { + const size = Size(32, 32); + if (attachment.thumbUrl != null) { + return Container( + height: size.height, + width: size.width, + decoration: BoxDecoration( + image: DecorationImage( + fit: BoxFit.cover, + image: CachedNetworkImageProvider( + attachment.thumbUrl!, + ), + ), + ), + ); + } + return AttachmentError(constraints: BoxConstraints.loose(size)); + } +} + +class _VideoAttachmentThumbnail extends StatefulWidget { + const _VideoAttachmentThumbnail({ + required this.attachment, + }); + + final Attachment attachment; + + @override + _VideoAttachmentThumbnailState createState() => + _VideoAttachmentThumbnailState(); +} + +class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> { + late VideoPlayerController _controller; + + @override + void initState() { + super.initState(); + _controller = VideoPlayerController.network(widget.attachment.assetUrl!) + ..initialize().then((_) { + // ignore: no-empty-block + setState(() {}); //when your thumbnail will show. + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 32, + width: 32, + child: _controller.value.isInitialized + ? VideoPlayer(_controller) + : const CircularProgressIndicator(), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/quoting_message_top_area.dart b/packages/stream_chat_flutter/lib/src/message_input/quoting_message_top_area.dart new file mode 100644 index 00000000..2a8b91b8 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/quoting_message_top_area.dart @@ -0,0 +1,60 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template quotingMessageTopArea} +/// The area that appears above [MessageInput] when the user is quoting a +/// message. +/// +/// Should only be used on mobile platforms. +/// {@endtemplate} +class QuotingMessageTopArea extends StatelessWidget { + /// {@macro quotingMessageTopArea} + const QuotingMessageTopArea({ + super.key, + required this.hasQuotedMessage, + this.onQuotedMessageCleared, + }); + + /// + final bool hasQuotedMessage; + + /// The callback to perform when the "close" button is tapped. + /// + /// Should be [MessageInput.onQuotedMessageCleared]. + final VoidCallback? onQuotedMessageCleared; + + @override + Widget build(BuildContext context) { + final _streamChatTheme = StreamChatTheme.of(context); + if (hasQuotedMessage) { + return Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: StreamSvgIcon.reply( + color: _streamChatTheme.colorTheme.disabled, + ), + ), + Text( + context.translations.replyToMessageLabel, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + IconButton( + visualDensity: VisualDensity.compact, + icon: StreamSvgIcon.closeSmall(), + onPressed: onQuotedMessageCleared?.call, + ), + ], + ), + ); + } else { + return const SizedBox.shrink(); + } + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/message_input/simple_safe_area.dart b/packages/stream_chat_flutter/lib/src/message_input/simple_safe_area.dart similarity index 100% rename from packages/stream_chat_flutter/lib/src/v4/message_input/simple_safe_area.dart rename to packages/stream_chat_flutter/lib/src/message_input/simple_safe_area.dart diff --git a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart similarity index 55% rename from packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart rename to packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart index db1c1925..9b54be85 100644 --- a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart @@ -3,133 +3,27 @@ import 'dart:async'; import 'dart:math'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:collection/collection.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/foundation.dart'; +import 'package:cached_network_image/cached_network_image.dart' + hide ErrorListener; +import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:image_picker/image_picker.dart'; +import 'package:photo_manager/photo_manager.dart'; import 'package:shimmer/shimmer.dart'; -import 'package:stream_chat_flutter/src/commands_overlay.dart'; -import 'package:stream_chat_flutter/src/emoji/emoji.dart'; -import 'package:stream_chat_flutter/src/emoji_overlay.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/quoted_message_widget.dart'; -import 'package:stream_chat_flutter/src/user_mentions_overlay.dart'; -import 'package:stream_chat_flutter/src/v4/message_input/simple_safe_area.dart'; -import 'package:stream_chat_flutter/src/v4/message_input/tld.dart'; -import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; +import 'package:stream_chat_flutter/platform_widget_builder/src/platform_widget_builder.dart'; +import 'package:stream_chat_flutter/src/message_input/attachment_button.dart'; +import 'package:stream_chat_flutter/src/message_input/command_button.dart'; +import 'package:stream_chat_flutter/src/message_input/dm_checkbox.dart'; +import 'package:stream_chat_flutter/src/message_input/quoted_message_widget.dart'; +import 'package:stream_chat_flutter/src/message_input/quoting_message_top_area.dart'; +import 'package:stream_chat_flutter/src/message_input/simple_safe_area.dart'; +import 'package:stream_chat_flutter/src/message_input/tld.dart'; +import 'package:stream_chat_flutter/src/utils/utils.dart'; +import 'package:stream_chat_flutter/src/video/video_thumbnail_image.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// A function that returns true if the message is valid and can be sent. -typedef MessageValidator = bool Function(Message message); - -/// A callback that can be passed to [StreamMessageInput.onError]. -/// -/// This callback should not throw. -/// -/// It exists merely for error reporting, and should not be used otherwise. -typedef ErrorListener = void Function( - Object error, - StackTrace? stackTrace, -); - -/// A callback that can be passed to -/// [StreamMessageInput.onAttachmentLimitExceed]. -/// -/// This callback should not throw. -/// -/// It exists merely for showing a custom error, and should not be used -/// otherwise. -typedef AttachmentLimitExceedListener = void Function( - int limit, - String error, -); - -/// Builder for attachment thumbnails. -typedef AttachmentThumbnailBuilder = Widget Function( - BuildContext, - Attachment, -); - -/// Builder function for building a mention tile. -typedef MentionTileBuilder = Widget Function( - BuildContext context, - Member member, -); - -/// Builder function for building a user mention tile. -/// -/// Use [StreamUserMentionTile] for the default implementation. -typedef UserMentionTileBuilder = Widget Function( - BuildContext context, - User user, -); - -/// Widget builder for action button. -/// -/// [defaultActionButton] is the default [IconButton] configuration, -/// use .copyWith to easily customize it. -typedef ActionButtonBuilder = Widget Function( - BuildContext context, - IconButton defaultActionButton, -); - -/// Widget builder for widgets that may require data from the -/// [StreamMessageInputController]. -typedef MessageRelatedBuilder = Widget Function( - BuildContext context, - StreamMessageInputController messageInputController, -); - -/// Widget builder for a custom attachment picker. -typedef AttachmentsPickerBuilder = Widget Function( - BuildContext context, - StreamMessageInputController messageInputController, - StreamAttachmentPicker defaultPicker, -); - -/// Location for actions on the [StreamMessageInput]. -enum ActionsLocation { - /// Align to left - left, - - /// Align to right - right, - - /// Align to left but inside the [TextField] - leftInside, - - /// Align to right but inside the [TextField] - rightInside, -} - -/// Default attachments for widget. -enum DefaultAttachmentTypes { - /// Image Attachment - image, - - /// Video Attachment - video, - - /// File Attachment - file, -} - -/// Available locations for the `sendMessage` button relative to the textField. -enum SendButtonLocation { - /// inside the textField - inside, - - /// outside the textField - outside, -} - -const _kMinMediaPickerSize = 360.0; - -/// Default maximum size for media attachments. -const kDefaultMaxAttachmentSize = 1024 * 1024 * 100; // 100MB in Bytes +const _kCommandTrigger = '/'; +const _kMentionTrigger = '@'; /// Inactive state: /// @@ -180,7 +74,11 @@ class StreamMessageInput extends StatefulWidget { this.onMessageSent, this.preMessageSending, this.maxHeight = 150, - this.keyboardType = TextInputType.multiline, + this.maxLines, + this.minLines, + this.textInputAction, + this.keyboardType, + this.textCapitalization = TextCapitalization.sentences, this.disableAttachments = false, this.messageInputController, this.actions = const [], @@ -200,9 +98,8 @@ class StreamMessageInput extends StatefulWidget { this.onAttachmentLimitExceed, this.attachmentButtonBuilder, this.commandButtonBuilder, - this.customOverlays = const [], + this.customAutocompleteTriggers = const [], this.mentionAllAppUsers = false, - this.attachmentsPickerBuilder, this.sendButtonBuilder, this.shouldKeepFocusAfterMessage, this.validator = _defaultValidator, @@ -211,14 +108,12 @@ class StreamMessageInput extends StatefulWidget { this.elevation, this.shadow, this.autoCorrect = true, - @Deprecated('Please use enableEmojiSuggestionsOverlay') - this.disableEmojiSuggestionsOverlay = false, - this.enableEmojiSuggestionsOverlay = true, this.enableMentionsOverlay = true, + this.onQuotedMessageCleared, }); - /// List of options for showing overlays. - final List customOverlays; + /// List of triggers for showing autocomplete. + final Iterable customAutocompleteTriggers; /// Max attachment size in bytes: /// - Defaults to 20 MB @@ -236,8 +131,20 @@ class StreamMessageInput extends StatefulWidget { /// Maximum Height for the TextField to grow before it starts scrolling. final double maxHeight; + /// The maximum lines of text the input can span. + final int? maxLines; + + /// The minimum lines of text the input can span. + final int? minLines; + + /// The type of action button to use for the keyboard. + final TextInputAction? textInputAction; + /// The keyboard type assigned to the TextField. - final TextInputType keyboardType; + final TextInputType? keyboardType; + + /// {@macro flutter.widgets.editableText.textCapitalization} + final TextCapitalization textCapitalization; /// If true the attachments button will not be displayed. final bool disableAttachments; @@ -291,24 +198,21 @@ class StreamMessageInput extends StatefulWidget { /// Builder for customizing the attachment button. /// - /// The builder contains the default [IconButton] that can be customized by - /// calling `.copyWith`. - final ActionButtonBuilder? attachmentButtonBuilder; + /// The builder contains the default [AttachmentButton] that can be customized + /// by calling `.copyWith`. + final AttachmentButtonBuilder? attachmentButtonBuilder; /// Builder for customizing the command button. /// - /// The builder contains the default [IconButton] that can be customized by + /// The builder contains the default [CommandButton] that can be customized by /// calling `.copyWith`. - final ActionButtonBuilder? commandButtonBuilder; + final CommandButtonBuilder? commandButtonBuilder; /// When enabled mentions search users across the entire app. /// /// Defaults to false. final bool mentionAllAppUsers; - /// Builds bottom sheet when attachment picker is opened. - final AttachmentsPickerBuilder? attachmentsPickerBuilder; - /// Builder for creating send button final MessageRelatedBuilder? sendButtonBuilder; @@ -335,19 +239,13 @@ class StreamMessageInput extends StatefulWidget { /// autoCorrect is enabled by default final bool autoCorrect; - /// Disable the default emoji suggestions - /// Enabled by default - @Deprecated('Please use enableEmojiSuggestionsOverlay') - final bool disableEmojiSuggestionsOverlay; - - /// Disable the default emoji suggestions by passing `false` - /// Enabled by default - final bool enableEmojiSuggestionsOverlay; - /// Disable the mentions overlay by passing false /// Enabled by default final bool enableMentionsOverlay; + /// Callback for when the quoted message is cleared + final VoidCallback? onQuotedMessageCleared; + static bool _defaultValidator(Message message) => message.text?.isNotEmpty == true || message.attachments.isNotEmpty; @@ -357,32 +255,29 @@ class StreamMessageInput extends StatefulWidget { /// State of [StreamMessageInput] class StreamMessageInputState extends State - with RestorationMixin { - final _imagePicker = ImagePicker(); - late FocusNode _focusNode = widget.focusNode ?? FocusNode(); - late final _isInternalFocusNode = widget.focusNode == null; - bool _inputEnabled = true; - - bool get _commandEnabled => _effectiveController.value.command != null; - bool _showCommandsOverlay = false; - bool _showMentionsOverlay = false; + with RestorationMixin, WidgetsBindingObserver { + bool get _commandEnabled => _effectiveController.message.command != null; bool _actionsShrunk = false; - bool _openFilePickerSection = false; late StreamChatThemeData _streamChatTheme; late StreamMessageInputThemeData _messageInputTheme; bool get _hasQuotedMessage => - _effectiveController.value.quotedMessage != null; + _effectiveController.message.quotedMessage != null; bool get _isEditing => - _effectiveController.value.status != MessageSendingStatus.sending; + _effectiveController.message.status != MessageSendingStatus.sending; - StreamRestorableMessageInputController? _controller; + BoxBorder? _draggingBorder; + + FocusNode get _effectiveFocusNode => + widget.focusNode ?? (_focusNode ??= FocusNode()); + FocusNode? _focusNode; StreamMessageInputController get _effectiveController => widget.messageInputController ?? _controller!.value; + StreamRestorableMessageInputController? _controller; void _createLocalController([Message? message]) { assert(_controller == null, ''); @@ -392,25 +287,60 @@ class StreamMessageInputState extends State void _registerController() { assert(_controller != null, ''); - registerForRestoration( - _controller!, - widget.restorationId ?? 'messageInputController', - ); - _effectiveController.textEditingController - .removeListener(_onChangedDebounced); - _effectiveController.textEditingController.addListener(_onChangedDebounced); + registerForRestoration(_controller!, 'messageInputController'); + _effectiveController + ..removeListener(_onChangedDebounced) + ..addListener(_onChangedDebounced); + if (!_isEditing && _timeOut <= 0) _startSlowMode(); + } + + void _initialiseEffectiveController() { + _effectiveController + ..removeListener(_onChangedDebounced) + ..addListener(_onChangedDebounced); if (!_isEditing && _timeOut <= 0) _startSlowMode(); } @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); if (widget.messageInputController == null) { _createLocalController(); } else { _initialiseEffectiveController(); } - _focusNode.addListener(_focusNodeListener); + _effectiveFocusNode.addListener(_focusNodeListener); + } + + @override + void didChangeDependencies() { + _streamChatTheme = StreamChatTheme.of(context); + _messageInputTheme = StreamMessageInputTheme.of(context); + super.didChangeDependencies(); + } + + bool _askingForPermission = false; + + @override + void didChangeAppLifecycleState(AppLifecycleState state) async { + if (state == AppLifecycleState.resumed && + _permissionState != null && + !_askingForPermission) { + _askingForPermission = true; + + try { + final newPermissionState = await PhotoManager.requestPermissionExtend(); + if (newPermissionState != _permissionState && mounted) { + setState(() { + _permissionState = newPermissionState; + }); + } + } catch (_) {} + + _askingForPermission = false; + } + super.didChangeAppLifecycleState(state); } @override @@ -418,7 +348,7 @@ class StreamMessageInputState extends State super.didUpdateWidget(oldWidget); if (widget.messageInputController == null && oldWidget.messageInputController != null) { - _createLocalController(oldWidget.messageInputController!.value); + _createLocalController(oldWidget.messageInputController!.message); } else if (widget.messageInputController != null && oldWidget.messageInputController == null) { unregisterFromRestoration(_controller!); @@ -428,10 +358,9 @@ class StreamMessageInputState extends State } // Update _focusNode - if (widget.focusNode != null && oldWidget.focusNode != widget.focusNode) { - _focusNode.removeListener(_focusNodeListener); - _focusNode = widget.focusNode!; - _focusNode.addListener(_focusNodeListener); + if (widget.focusNode != oldWidget.focusNode) { + (oldWidget.focusNode ?? _focusNode)?.removeListener(_focusNodeListener); + (widget.focusNode ?? _focusNode)?.addListener(_focusNodeListener); } } @@ -445,21 +374,13 @@ class StreamMessageInputState extends State @override String? get restorationId => widget.restorationId; - void _focusNodeListener() { - if (_focusNode.hasFocus) { - _openFilePickerSection = false; - } - } + // ignore: no-empty-block + void _focusNodeListener() {} int _timeOut = 0; Timer? _slowModeTimer; - void _initialiseEffectiveController() { - _effectiveController.textEditingController - .removeListener(_onChangedDebounced); - _effectiveController.textEditingController.addListener(_onChangedDebounced); - if (!_isEditing && _timeOut <= 0) _startSlowMode(); - } + PermissionState? _permissionState; void _startSlowMode() { if (!mounted) { @@ -506,6 +427,7 @@ class StreamMessageInputState extends State ), ); } + return StreamMessageValueListenableBuilder( valueListenable: _effectiveController, builder: (context, value, _) { @@ -525,42 +447,19 @@ class StreamMessageInputState extends State child: GestureDetector( onPanUpdate: (details) { if (details.delta.dy > 0) { - _focusNode.unfocus(); - if (_openFilePickerSection) { - setState(() { - _openFilePickerSection = false; - }); - } + _effectiveFocusNode.unfocus(); } }, child: Column( mainAxisSize: MainAxisSize.min, children: [ if (_hasQuotedMessage) - Padding( - padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.all(8), - child: StreamSvgIcon.reply( - color: _streamChatTheme.colorTheme.disabled, - ), - ), - Text( - context.translations.replyToMessageLabel, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - IconButton( - visualDensity: VisualDensity.compact, - icon: StreamSvgIcon.closeSmall(), - onPressed: () { - _effectiveController.clearQuotedMessage(); - _focusNode.unfocus(); - }, - ), - ], + // Ensure this doesn't show on web & desktop + PlatformWidgetBuilder( + mobile: (context, child) => child, + child: QuotingMessageTopArea( + hasQuotedMessage: _hasQuotedMessage, + onQuotedMessageCleared: widget.onQuotedMessageCleared, ), ) else if (_effectiveController.ogAttachment != null) @@ -568,14 +467,14 @@ class StreamMessageInputState extends State attachment: _effectiveController.ogAttachment!, onDismissPreviewPressed: () { _effectiveController.clearOGAttachment(); - _focusNode.unfocus(); + _effectiveFocusNode.unfocus(); }, ), Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: _buildTextField(context), ), - if (_effectiveController.value.parentId != null && + if (_effectiveController.message.parentId != null && !widget.hideSendAsDm) Padding( padding: const EdgeInsets.only( @@ -583,9 +482,33 @@ class StreamMessageInputState extends State left: 12, bottom: 12, ), - child: _buildDmCheckbox(), + child: DmCheckbox( + foregroundDecoration: BoxDecoration( + border: _effectiveController.showInChannel + ? null + : Border.all( + color: _streamChatTheme + .colorTheme.textHighEmphasis + .withOpacity(0.5), + width: 2, + ), + borderRadius: BorderRadius.circular(3), + ), + color: _effectiveController.showInChannel + ? _streamChatTheme.colorTheme.accentPrimary + : _streamChatTheme.colorTheme.barsBg, + onTap: () { + _effectiveController.showInChannel = + !_effectiveController.showInChannel; + }, + crossFadeState: _effectiveController.showInChannel + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + ), ), - _buildFilePickerSection(), + // PlatformWidgetBuilder( + // mobile: (context, child) => _buildFilePickerSection(), + // ), ], ), ), @@ -600,113 +523,78 @@ class StreamMessageInputState extends State child: child, ); } - return StreamMultiOverlay( - childAnchor: Alignment.topCenter, - overlayAnchor: Alignment.bottomCenter, - overlayOptions: [ - OverlayOptions( - visible: _showCommandsOverlay, - widget: _buildCommandsOverlayEntry(), + + return StreamAutocomplete( + focusNode: _effectiveFocusNode, + messageEditingController: _effectiveController, + fieldViewBuilder: (_, __, ___) => child, + autocompleteTriggers: [ + ...widget.customAutocompleteTriggers, + StreamAutocompleteTrigger( + trigger: _kCommandTrigger, + triggerOnlyAtStart: true, + optionsViewBuilder: ( + context, + autocompleteQuery, + messageEditingController, + ) { + final query = autocompleteQuery.query; + return StreamCommandAutocompleteOptions( + query: query, + channel: StreamChannel.of(context).channel, + onCommandSelected: (command) { + _effectiveController.command = command.name; + // removing the overlay after the command is selected + StreamAutocomplete.of(context).closeSuggestions(); + }, + ); + }, ), - if (widget.enableEmojiSuggestionsOverlay && - !widget.disableEmojiSuggestionsOverlay) - OverlayOptions( - visible: _focusNode.hasFocus && - _effectiveController.text.isNotEmpty && - _effectiveController.baseOffset > 0 && - _effectiveController.text - .substring( - 0, - _effectiveController.baseOffset, - ) - .contains(':'), - widget: _buildEmojiOverlay(), - ), if (widget.enableMentionsOverlay) - OverlayOptions( - visible: _showMentionsOverlay, - widget: _buildMentionsOverlayEntry(), + StreamAutocompleteTrigger( + trigger: _kMentionTrigger, + optionsViewBuilder: ( + context, + autocompleteQuery, + messageEditingController, + ) { + final query = autocompleteQuery.query; + return StreamMentionAutocompleteOptions( + query: query, + channel: StreamChannel.of(context).channel, + mentionAllAppUsers: widget.mentionAllAppUsers, + mentionsTileBuilder: widget.userMentionsTileBuilder, + onMentionUserTap: (user) { + // adding the mentioned user to the controller. + _effectiveController.addMentionedUser(user); + + // accepting the autocomplete option. + StreamAutocomplete.of(context) + .acceptAutocompleteOption(user.name); + }, + ); + }, ), - ...widget.customOverlays, ], - child: child, ); }, ); } - Flex _buildTextField(BuildContext context) => Flex( - direction: Axis.horizontal, - children: [ - if (!_commandEnabled && - widget.actionsLocation == ActionsLocation.left) - _buildExpandActionsButton(context), - _buildTextInput(context), - if (!_commandEnabled && - widget.actionsLocation == ActionsLocation.right) - _buildExpandActionsButton(context), - if (widget.sendButtonLocation == SendButtonLocation.outside) - _buildSendButton(context), - ], - ); - - Widget _buildDmCheckbox() => Row( - children: [ - Container( - height: 16, - width: 16, - foregroundDecoration: BoxDecoration( - border: _effectiveController.showInChannel - ? null - : Border.all( - color: _streamChatTheme.colorTheme.textHighEmphasis - .withOpacity(0.5), - width: 2, - ), - borderRadius: BorderRadius.circular(3), - ), - child: Center( - child: Material( - borderRadius: BorderRadius.circular(3), - color: _effectiveController.showInChannel - ? _streamChatTheme.colorTheme.accentPrimary - : _streamChatTheme.colorTheme.barsBg, - child: InkWell( - onTap: () { - _effectiveController.showInChannel = - !_effectiveController.showInChannel; - }, - child: AnimatedCrossFade( - duration: const Duration(milliseconds: 300), - reverseDuration: const Duration(milliseconds: 300), - crossFadeState: _effectiveController.showInChannel - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - firstChild: StreamSvgIcon.check( - size: 16, - color: _streamChatTheme.colorTheme.barsBg, - ), - secondChild: const SizedBox( - height: 16, - width: 16, - ), - ), - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Text( - context.translations.alsoSendAsDirectMessageLabel, - style: _streamChatTheme.textTheme.footnote.copyWith( - color: _streamChatTheme.colorTheme.textHighEmphasis - .withOpacity(0.5), - ), - ), - ), - ], - ); + Flex _buildTextField(BuildContext context) { + return Flex( + direction: Axis.horizontal, + children: [ + if (!_commandEnabled && widget.actionsLocation == ActionsLocation.left) + _buildExpandActionsButton(context), + _buildTextInput(context), + if (!_commandEnabled && widget.actionsLocation == ActionsLocation.right) + _buildExpandActionsButton(context), + if (widget.sendButtonLocation == SendButtonLocation.outside) + _buildSendButton(context), + ], + ); + } Widget _buildSendButton(BuildContext context) { if (widget.sendButtonBuilder != null) { @@ -779,6 +667,33 @@ class StreamMessageInputState extends State ); } + Widget _buildAttachmentButton(BuildContext context) { + final defaultButton = AttachmentButton( + color: _messageInputTheme.actionButtonIdleColor!, + onPressed: _onAttachmentButtonPressed, + ); + + return widget.attachmentButtonBuilder?.call(context, defaultButton) ?? + defaultButton; + } + + /// Handle the platform-specific logic for selecting files. + /// + /// On mobile, this will open the file selection bottom sheet. On desktop, + /// this will open the native file system and allow the user to select one + /// or more files. + Future _onAttachmentButtonPressed() async { + final attachments = await showStreamAttachmentPickerModalBottomSheet( + context: context, + initialAttachments: _effectiveController.attachments, + useRootNavigator: true, + ); + + if (attachments != null) { + _effectiveController.attachments = attachments; + } + } + Expanded _buildTextInput(BuildContext context) { final margin = (widget.sendButtonLocation == SendButtonLocation.inside ? const EdgeInsets.only(right: 8) @@ -786,49 +701,96 @@ class StreamMessageInputState extends State (widget.actionsLocation != ActionsLocation.left || _commandEnabled ? const EdgeInsets.only(left: 8) : EdgeInsets.zero); + return Expanded( - child: Container( - clipBehavior: Clip.hardEdge, - margin: margin, - decoration: BoxDecoration( - borderRadius: _messageInputTheme.borderRadius, - gradient: _focusNode.hasFocus - ? _messageInputTheme.activeBorderGradient - : _messageInputTheme.idleBorderGradient, - color: _messageInputTheme.inputBackgroundColor, - ), - child: Padding( - padding: const EdgeInsets.all(1.5), - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: _messageInputTheme.borderRadius, - color: _messageInputTheme.inputBackgroundColor, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildReplyToMessage(), - _buildAttachments(), - LimitedBox( - maxHeight: widget.maxHeight, - child: StreamMessageTextField( - key: const Key('messageInputText'), - enabled: _inputEnabled, - maxLines: null, - onSubmitted: (_) => sendMessage(), - keyboardType: widget.keyboardType, - controller: _effectiveController, - focusNode: _focusNode, - style: _messageInputTheme.inputTextStyle, - autofocus: widget.autofocus, - textAlignVertical: TextAlignVertical.center, - decoration: _getInputDecoration(context), - textCapitalization: TextCapitalization.sentences, - autocorrect: widget.autoCorrect, + child: DropTarget( + onDragDone: (details) async { + final files = details.files; + final attachments = []; + for (final file in files) { + final attachment = await file.toAttachment(type: 'file'); + attachments.add(attachment); + } + + if (attachments.isNotEmpty) _addAttachments(attachments); + }, + onDragEntered: (details) { + setState(() { + _draggingBorder = Border.all( + color: _streamChatTheme.colorTheme.accentPrimary, + ); + }); + }, + onDragExited: (details) { + setState(() => _draggingBorder = null); + }, + child: Container( + clipBehavior: Clip.hardEdge, + margin: margin, + decoration: BoxDecoration( + borderRadius: _messageInputTheme.borderRadius, + gradient: _effectiveFocusNode.hasFocus + ? _messageInputTheme.activeBorderGradient + : _messageInputTheme.idleBorderGradient, + border: _draggingBorder, + ), + child: Padding( + padding: const EdgeInsets.all(1.5), + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: _messageInputTheme.borderRadius, + color: _messageInputTheme.inputBackgroundColor, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildReplyToMessage(), + _buildAttachments(), + LimitedBox( + maxHeight: widget.maxHeight, + child: PlatformWidgetBuilder( + web: (context, child) => KeyboardShortcutRunner( + onEnterKeypress: sendMessage, + onEscapeKeypress: () { + if (_hasQuotedMessage && + _effectiveController.text.isEmpty) { + widget.onQuotedMessageCleared?.call(); + } + }, + child: child!, + ), + desktop: (context, child) => KeyboardShortcutRunner( + onEnterKeypress: sendMessage, + onEscapeKeypress: () { + if (_hasQuotedMessage && + _effectiveController.text.isEmpty) { + widget.onQuotedMessageCleared?.call(); + } + }, + child: child!, + ), + mobile: (context, child) => child, + child: StreamMessageTextField( + key: const Key('messageInputText'), + maxLines: widget.maxLines, + minLines: widget.minLines, + textInputAction: widget.textInputAction, + onSubmitted: (_) => sendMessage(), + keyboardType: widget.keyboardType, + controller: _effectiveController, + focusNode: _effectiveFocusNode, + style: _messageInputTheme.inputTextStyle, + autofocus: widget.autofocus, + textAlignVertical: TextAlignVertical.center, + decoration: _getInputDecoration(context), + textCapitalization: widget.textCapitalization, + autocorrect: widget.autoCorrect, + ), + ), ), - ), - ], + ], + ), ), ), ), @@ -891,7 +853,7 @@ class StreamMessageInputState extends State size: 16, ), Text( - _effectiveController.value.command!.toUpperCase(), + _effectiveController.message.command!.toUpperCase(), style: _streamChatTheme.textTheme.footnoteBold.copyWith( color: Colors.white, @@ -959,21 +921,16 @@ class StreamMessageInputState extends State if (widget.showCommandsButton) actionsLength += 1; if (!widget.disableAttachments) actionsLength += 1; - setState(() { - _actionsShrunk = value.isNotEmpty && actionsLength > 1; - }); + setState(() => _actionsShrunk = value.isNotEmpty && actionsLength > 1); _checkContainsUrl(value, context); - _checkCommands(value, context); - _checkMentions(value, context); - _checkEmoji(value, context); }, const Duration(milliseconds: 350), leading: true, ); String _getHint(BuildContext context) { - if (_commandEnabled && _effectiveController.value.command == 'giphy') { + if (_commandEnabled && _effectiveController.message.command == 'giphy') { return context.translations.searchGifLabel; } if (_effectiveController.attachments.isNotEmpty) { @@ -1055,195 +1012,17 @@ class StreamMessageInputState extends State return response; } - void _checkEmoji(String value, BuildContext context) { - if (value.isNotEmpty && - _effectiveController.baseOffset > 0 && - _effectiveController.text - .substring(0, _effectiveController.baseOffset) - .contains(':')) { - final textToSelection = _effectiveController.text.substring( - 0, - _effectiveController.selectionStart, - ); - final splits = textToSelection.split(':'); - final query = splits[splits.length - 2].toLowerCase(); - final emoji = Emoji.byName(query); - - if (textToSelection.endsWith(':') && emoji != null) { - _chooseEmoji(splits.sublist(0, splits.length - 1), emoji); - } - } - } - - void _checkMentions(String value, BuildContext context) { - if (value.isNotEmpty && - _effectiveController.baseOffset > 0 && - _effectiveController.text - .substring(0, _effectiveController.baseOffset) - .split(' ') - .last - .contains('@')) { - if (!_showMentionsOverlay) { - setState(() { - _showMentionsOverlay = true; - }); - } - } else if (_showMentionsOverlay) { - setState(() { - _showMentionsOverlay = false; - }); - } - } - - void _checkCommands(String value, BuildContext context) { - if (value.startsWith('/')) { - final allCommands = StreamChannel.of(context).channel.config?.commands; - final command = - allCommands?.firstWhereOrNull((it) => it.name == value.substring(1)); - if (command != null) { - return _setCommand(command); - } else if (!_showCommandsOverlay) { - setState(() { - _showCommandsOverlay = true; - }); - } - } else if (_showCommandsOverlay) { - setState(() { - _showCommandsOverlay = false; - }); - } - } - - Widget _buildCommandsOverlayEntry() { - final text = _effectiveController.text.trimLeft(); - - final renderObject = context.findRenderObject() as RenderBox?; - if (renderObject == null) { - return const Offstage(); - } - return StreamCommandsOverlay( - channel: StreamChannel.of(context).channel, - size: Size(renderObject.size.width - 16, 400), - text: text, - onCommandResult: _setCommand, - ); - } - - Widget _buildFilePickerSection() { - final picker = StreamAttachmentPicker( - messageInputController: _effectiveController, - onFilePicked: pickFile, - isOpen: _openFilePickerSection, - pickerSize: _openFilePickerSection ? _kMinMediaPickerSize : 0, - attachmentLimit: widget.attachmentLimit, - onAttachmentLimitExceeded: widget.onAttachmentLimitExceed, - maxAttachmentSize: widget.maxAttachmentSize, - onError: _showErrorAlert, - ); - - if (_openFilePickerSection && widget.attachmentsPickerBuilder != null) { - return widget.attachmentsPickerBuilder!( - context, - _effectiveController, - picker, - ); - } - - return picker; - } - - Widget _buildMentionsOverlayEntry() { - final channel = StreamChannel.of(context).channel; - if (_effectiveController.selectionStart < 0 || channel.state == null) { - return const Offstage(); - } - - final splits = _effectiveController.text - .substring(0, _effectiveController.selectionStart) - .split('@'); - final query = splits.last.toLowerCase(); - - // ignore: cast_nullable_to_non_nullable - final renderObject = context.findRenderObject() as RenderBox; - - return LayoutBuilder( - builder: (context, snapshot) => StreamUserMentionsOverlay( - query: query, - mentionAllAppUsers: widget.mentionAllAppUsers, - client: StreamChat.of(context).client, - channel: channel, - size: Size( - renderObject.size.width - 16, - min(400, (snapshot.maxHeight - renderObject.size.height - 16).abs()), - ), - mentionsTileBuilder: widget.userMentionsTileBuilder, - onMentionUserTap: (user) { - _effectiveController.addMentionedUser(user); - splits[splits.length - 1] = user.name; - final rejoin = splits.join('@'); - - _effectiveController.text = - '$rejoin${_effectiveController.text.substring( - _effectiveController.selectionStart, - )}'; - - _onChangedDebounced.cancel(); - setState(() => _showMentionsOverlay = false); - }, - ), - ); - } - - Widget _buildEmojiOverlay() { - if (_effectiveController.baseOffset < 0) { - return const Offstage(); - } - - final splits = _effectiveController.text - .substring(0, _effectiveController.baseOffset) - .split(':'); - - final query = splits.last.toLowerCase(); - // ignore: cast_nullable_to_non_nullable - final renderObject = context.findRenderObject() as RenderBox; - - return StreamEmojiOverlay( - size: Size(renderObject.size.width - 16, 200), - query: query, - onEmojiResult: (emoji) { - _chooseEmoji(splits, emoji); - }, - ); - } - - void _chooseEmoji(List splits, Emoji emoji) { - final rejoin = splits.sublist(0, splits.length - 1).join(':') + emoji.char!; - - _effectiveController.text = rejoin + - _effectiveController.text.substring( - _effectiveController.selectionStart, - ); - } - - void _setCommand(Command c) { - _effectiveController - ..reset() - ..command = c; - setState(() { - _showCommandsOverlay = false; - }); - } - Widget _buildReplyToMessage() { if (!_hasQuotedMessage) return const Offstage(); - final containsUrl = _effectiveController.value.quotedMessage!.attachments + final containsUrl = _effectiveController.message.quotedMessage!.attachments .any((element) => element.titleLink != null); return StreamQuotedMessageWidget( reverse: true, showBorder: !containsUrl, - message: _effectiveController.value.quotedMessage!, + message: _effectiveController.message.quotedMessage!, messageTheme: _streamChatTheme.otherMessageTheme, padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + onQuotedMessageClear: widget.onQuotedMessageCleared, ); } @@ -1275,10 +1054,10 @@ class StreamMessageInputState extends State child: StreamFileAttachment( message: Message(), // dummy message attachment: e, - size: Size( + constraints: BoxConstraints.loose(Size( MediaQuery.of(context).size.width * 0.65, 56, - ), + )), trailing: Padding( padding: const EdgeInsets.all(8), child: _buildRemoveButton(e), @@ -1328,30 +1107,41 @@ class StreamMessageInputState extends State ); } - Widget _buildRemoveButton(Attachment attachment) => SizedBox( - height: 24, - width: 24, - child: RawMaterialButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - elevation: 0, - highlightElevation: 0, - focusElevation: 0, - hoverElevation: 0, - onPressed: () { - _effectiveController.removeAttachmentById(attachment.id); - }, - fillColor: - _streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.5), - child: Center( - child: StreamSvgIcon.close( - size: 24, - color: _streamChatTheme.colorTheme.barsBg, - ), + Widget _buildRemoveButton(Attachment attachment) { + return SizedBox( + height: 24, + width: 24, + child: RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + highlightElevation: 0, + focusElevation: 0, + hoverElevation: 0, + onPressed: () async { + final file = attachment.file; + final uploadState = attachment.uploadState; + + if (file != null && !uploadState.isSuccess && !isWeb) { + await StreamAttachmentHandler.instance.deleteAttachmentFile( + attachmentFile: file, + ); + } + + _effectiveController.removeAttachmentById(attachment.id); + }, + fillColor: + _streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.5), + child: Center( + child: StreamSvgIcon.close( + size: 24, + color: _streamChatTheme.colorTheme.barsBg, ), ), - ); + ), + ); + } Widget _buildAttachment(Attachment attachment) { if (widget.attachmentThumbnailBuilders?.containsKey(attachment.type) == @@ -1395,8 +1185,12 @@ class StreamMessageInputState extends State return Stack( children: [ StreamVideoThumbnailImage( - height: 104, - width: 104, + constraints: BoxConstraints.loose( + const Size( + 104, + 104, + ), + ), video: (attachment.file?.path ?? attachment.assetUrl)!, fit: BoxFit.cover, ), @@ -1420,29 +1214,26 @@ class StreamMessageInputState extends State Widget _buildCommandButton(BuildContext context) { final s = _effectiveController.text.trim(); - final defaultButton = IconButton( - icon: StreamSvgIcon.lightning( - color: s.isNotEmpty - ? _streamChatTheme.colorTheme.disabled - : (_showCommandsOverlay - ? _messageInputTheme.actionButtonColor - : _messageInputTheme.actionButtonIdleColor), - ), - padding: EdgeInsets.zero, - constraints: const BoxConstraints.tightFor( - height: 24, - width: 24, - ), - splashRadius: 24, + final isCommandOptionsVisible = s.startsWith(_kCommandTrigger); + final defaultButton = CommandButton( + color: s.isNotEmpty + ? _streamChatTheme.colorTheme.disabled + : (isCommandOptionsVisible + ? _messageInputTheme.actionButtonColor! + : _messageInputTheme.actionButtonIdleColor!), onPressed: () async { - if (_openFilePickerSection) { - setState(() => _openFilePickerSection = false); - await Future.delayed(const Duration(milliseconds: 300)); + // Clear the text if the commands options are already visible. + if (isCommandOptionsVisible) { + _effectiveController.clear(); + _effectiveFocusNode.unfocus(); + } else { + // This triggers the [StreamAutocomplete] to show the command trigger. + _effectiveController.textEditingValue = const TextEditingValue( + text: _kCommandTrigger, + selection: TextSelection.collapsed(offset: _kCommandTrigger.length), + ); + _effectiveFocusNode.requestFocus(); } - - setState(() { - _showCommandsOverlay = !_showCommandsOverlay; - }); }, ); @@ -1450,98 +1241,6 @@ class StreamMessageInputState extends State defaultButton; } - Widget _buildAttachmentButton(BuildContext context) { - final defaultButton = IconButton( - icon: StreamSvgIcon.attach( - color: _openFilePickerSection - ? _messageInputTheme.actionButtonColor - : _messageInputTheme.actionButtonIdleColor, - ), - padding: EdgeInsets.zero, - constraints: const BoxConstraints.tightFor( - height: 24, - width: 24, - ), - splashRadius: 24, - onPressed: () async { - _showCommandsOverlay = false; - _showMentionsOverlay = false; - - if (_openFilePickerSection) { - setState(() => _openFilePickerSection = false); - } else { - showAttachmentModal(); - } - }, - ); - - return widget.attachmentButtonBuilder?.call(context, defaultButton) ?? - defaultButton; - } - - /// Show the attachment modal, making the user choose where to - /// pick a media from - void showAttachmentModal() { - if (_focusNode.hasFocus) { - _focusNode.unfocus(); - } - - if (!kIsWeb) { - setState(() { - _openFilePickerSection = true; - }); - } else { - showModalBottomSheet( - clipBehavior: Clip.hardEdge, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(32), - topRight: Radius.circular(32), - ), - ), - context: context, - isScrollControlled: true, - builder: (_) => Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - title: Text( - context.translations.addAFileLabel, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - ListTile( - leading: const Icon(Icons.image), - title: Text(context.translations.uploadAPhotoLabel), - onTap: () { - pickFile(DefaultAttachmentTypes.image); - Navigator.pop(context); - }, - ), - ListTile( - leading: const Icon(Icons.video_library), - title: Text(context.translations.uploadAVideoLabel), - onTap: () { - pickFile(DefaultAttachmentTypes.video); - Navigator.pop(context); - }, - ), - ListTile( - leading: const Icon(Icons.insert_drive_file), - title: Text(context.translations.uploadAFileLabel), - onTap: () { - pickFile(DefaultAttachmentTypes.file); - Navigator.pop(context); - }, - ), - ], - ), - ); - } - } - /// Adds an attachment to the [messageInputController.attachments] map void _addAttachments(Iterable attachments) { final limit = widget.attachmentLimit; @@ -1563,104 +1262,22 @@ class StreamMessageInputState extends State } } - /// Pick a file from the device - /// If [camera] is true then the camera will open - void pickFile( - DefaultAttachmentTypes fileType, { - bool camera = false, - }) async { - setState(() => _inputEnabled = false); - - AttachmentFile? file; - String? attachmentType; - - if (fileType == DefaultAttachmentTypes.image) { - attachmentType = 'image'; - } else if (fileType == DefaultAttachmentTypes.video) { - attachmentType = 'video'; - } else if (fileType == DefaultAttachmentTypes.file) { - attachmentType = 'file'; - } - - if (camera) { - XFile? pickedFile; - if (fileType == DefaultAttachmentTypes.image) { - pickedFile = await _imagePicker.pickImage(source: ImageSource.camera); - } else if (fileType == DefaultAttachmentTypes.video) { - pickedFile = await _imagePicker.pickVideo(source: ImageSource.camera); - } - if (pickedFile != null) { - final bytes = await pickedFile.readAsBytes(); - file = AttachmentFile( - size: bytes.length, - path: pickedFile.path, - bytes: bytes, - ); - } - } else { - late FileType type; - if (fileType == DefaultAttachmentTypes.image) { - type = FileType.image; - } else if (fileType == DefaultAttachmentTypes.video) { - type = FileType.video; - } else if (fileType == DefaultAttachmentTypes.file) { - type = FileType.any; - } - final res = await FilePicker.platform.pickFiles( - type: type, - ); - if (res?.files.isNotEmpty == true) { - file = res!.files.single.toAttachmentFile; - } - } - - setState(() => _inputEnabled = true); - - if (file == null) return; - - final mimeType = file.name?.mimeType ?? file.path!.split('/').last.mimeType; - - final extraDataMap = {}; - - if (mimeType?.subtype != null) { - extraDataMap['mime_type'] = mimeType!.subtype.toLowerCase(); - } - - extraDataMap['file_size'] = file.size!; - - final attachment = Attachment( - file: file, - type: attachmentType, - uploadState: const UploadState.preparing(), - extraData: extraDataMap, - ); - - if (file.size! > widget.maxAttachmentSize) { - return _showErrorAlert( - context.translations.fileTooLargeError( - widget.maxAttachmentSize / (1024 * 1024), - ), - ); - } - - _addAttachments([ - attachment.copyWith( - file: file, - extraData: {...attachment.extraData} - ..update('file_size', ((_) => file!.size!)), - ), - ]); - } - /// Sends the current message Future sendMessage() async { + if (_timeOut > 0 || + (_effectiveController.text.trim().isEmpty && + _effectiveController.attachments.isEmpty)) { + return; + } + final streamChannel = StreamChannel.of(context); var message = _effectiveController.value; + if (!streamChannel.channel.ownCapabilities .contains(PermissionType.sendLinks) && _urlRegex.allMatches(message.text ?? '').any((element) => element.group(0)?.split('.').last.isValidTLD() == true)) { - showInfoDialog( + showInfoBottomSheet( context, icon: StreamSvgIcon.error( color: StreamChatTheme.of(context).colorTheme.accentError, @@ -1673,11 +1290,19 @@ class StreamMessageInputState extends State return; } + final containsCommand = message.command != null; + // If the message contains command we should append it to the text + // before sending it. + if (containsCommand) { + message = message.copyWith(text: '/${message.command} ${message.text}'); + } + final skipEnrichUrl = _effectiveController.ogAttachment == null; var shouldKeepFocus = widget.shouldKeepFocusAfterMessage; shouldKeepFocus ??= !_commandEnabled; + widget.onQuotedMessageCleared?.call(); _effectiveController.reset(); @@ -1707,14 +1332,14 @@ class StreamMessageInputState extends State } if (shouldKeepFocus) { - FocusScope.of(context).requestFocus(_focusNode); + FocusScope.of(context).requestFocus(_effectiveFocusNode); } else { FocusScope.of(context).unfocus(); } final resp = await sendingFuture; if (resp.message?.type == 'error') { - _effectiveController.value = message; + _effectiveController.message = message; } _startSlowMode(); widget.onMessageSent?.call(resp.message); @@ -1737,81 +1362,23 @@ class StreamMessageInputState extends State topRight: Radius.circular(16), ), ), - builder: (context) => Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox( - height: 26, - ), - StreamSvgIcon.error( - color: _streamChatTheme.colorTheme.accentError, - size: 24, - ), - const SizedBox( - height: 26, - ), - Text( - context.translations.somethingWentWrongError, - style: _streamChatTheme.textTheme.headlineBold, - ), - const SizedBox( - height: 7, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - description, - textAlign: TextAlign.center, - ), - ), - const SizedBox( - height: 36, - ), - Container( - color: - _streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.08), - height: 1, - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text( - context.translations.okLabel, - style: _streamChatTheme.textTheme.bodyBold.copyWith( - color: _streamChatTheme.colorTheme.accentPrimary, - ), - ), - ), - ], - ), - ], + builder: (context) => ErrorAlertSheet( + errorDescription: context.translations.somethingWentWrongError, ), ); } @override void dispose() { - _effectiveController.textEditingController - .removeListener(_onChangedDebounced); + _effectiveController.removeListener(_onChangedDebounced); _controller?.dispose(); - _focusNode.removeListener(_focusNodeListener); - if (_isInternalFocusNode) _focusNode.dispose(); + _effectiveFocusNode.removeListener(_focusNodeListener); + _focusNode?.dispose(); _stopSlowMode(); _onChangedDebounced.cancel(); + WidgetsBinding.instance.removeObserver(this); super.dispose(); } - - @override - void didChangeDependencies() { - _streamChatTheme = StreamChatTheme.of(context); - _messageInputTheme = StreamMessageInputTheme.of(context); - - super.didChangeDependencies(); - } } /// Preview of an Open Graph attachment. diff --git a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_send_button.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_send_button.dart similarity index 100% rename from packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_send_button.dart rename to packages/stream_chat_flutter/lib/src/message_input/stream_message_send_button.dart diff --git a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_text_field.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart similarity index 91% rename from packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_text_field.dart rename to packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart index b9aa5df6..5a0fd63a 100644 --- a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_message_text_field.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart @@ -89,16 +89,10 @@ class StreamMessageTextField extends StatefulWidget { SmartDashesType? smartDashesType, SmartQuotesType? smartQuotesType, this.enableSuggestions = true, - this.maxLines = 1, + this.maxLines, this.minLines, this.expands = false, this.maxLength, - @Deprecated( - 'Use maxLengthEnforcement parameter which provides more specific ' - 'behavior related to the maxLength limit. ' - 'This feature was deprecated after v1.25.0-5.0.pre.', - ) - this.maxLengthEnforced = true, this.maxLengthEnforcement, this.onChanged, this.onEditingComplete, @@ -115,30 +109,25 @@ class StreamMessageTextField extends StatefulWidget { this.keyboardAppearance, this.scrollPadding = const EdgeInsets.all(20), this.dragStartBehavior = DragStartBehavior.start, - this.enableInteractiveSelection = true, + bool? enableInteractiveSelection, this.selectionControls, this.onTap, this.mouseCursor, this.buildCounter, this.scrollController, this.scrollPhysics, - this.autofillHints, + this.autofillHints = const [], + this.clipBehavior = Clip.hardEdge, this.restorationId, + this.scribbleEnabled = true, this.enableIMEPersonalizedLearning = true, - }) : assert(obscuringCharacter.length == 1, - '`obscuringCharacter.length` must be 1'), + }) : assert(obscuringCharacter.length == 1, ''), smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled), - assert( - maxLengthEnforced || maxLengthEnforcement == null, - 'maxLengthEnforced is deprecated, use only maxLengthEnforcement', - ), - assert(maxLines == null || maxLines > 0, - '`maxLines` needs to be left as null or bigger than 0'), - assert(minLines == null || minLines > 0, - '`minLines` needs to be left as null or bigger than 0'), + assert(maxLines == null || maxLines > 0, ''), + assert(minLines == null || minLines > 0, ''), assert( (maxLines == null) || (minLines == null) || (maxLines >= minLines), "minLines can't be greater than maxLines", @@ -153,7 +142,7 @@ class StreamMessageTextField extends StatefulWidget { maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0, - '`maxLength` needs to be null or a positive integer'), + 'maxLength must be null or a positive integer.'), // Assert the following instead of setting it directly to avoid // surprising the user by silently changing the value they set. @@ -161,22 +150,36 @@ class StreamMessageTextField extends StatefulWidget { !identical(textInputAction, TextInputAction.newline) || maxLines == 1 || !identical(keyboardType, TextInputType.text), - '''Use keyboardType TextInputType.multiline when using TextInputAction.newline on a multiline TextField.''', + 'Use keyboardType TextInputType.multiline when using TextInputAction.newline on a multiline TextField.', ), keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline), + enableInteractiveSelection = + enableInteractiveSelection ?? (!readOnly || !obscureText), toolbarOptions = toolbarOptions ?? (obscureText - ? const ToolbarOptions( - selectAll: true, - paste: true, - ) - : const ToolbarOptions( - copy: true, - cut: true, - selectAll: true, - paste: true, - )); + ? (readOnly + // No point in even offering "Select All" in a read-only obscured + // field. + ? const ToolbarOptions() + // Writable, but obscured. + : const ToolbarOptions( + selectAll: true, + paste: true, + )) + : (readOnly + // Read-only, not obscured. + ? const ToolbarOptions( + selectAll: true, + copy: true, + ) + // Writable, not obscured. + : const ToolbarOptions( + copy: true, + cut: true, + selectAll: true, + paste: true, + ))); /// Controls the message being edited. /// @@ -345,19 +348,6 @@ class StreamMessageTextField extends StatefulWidget { /// {@macro flutter.services.lengthLimitingTextInputFormatter.maxLength} final int? maxLength; - /// If [maxLength] is set, [maxLengthEnforced] indicates whether or not to - /// enforce the limit, or merely provide a character counter and warning when - /// [maxLength] is exceeded. - /// - /// If true, prevents the field from allowing more than [maxLength] - /// characters. - @Deprecated( - 'Use maxLengthEnforcement parameter which provides more specific ' - 'behavior related to the maxLength limit. ' - 'This feature was deprecated after v1.25.0-5.0.pre.', - ) - final bool maxLengthEnforced; - /// Determines how the [maxLength] limit should be enforced. /// /// {@macro flutter.services.textFormatter.effectiveMaxLengthEnforcement} @@ -435,8 +425,7 @@ class StreamMessageTextField extends StatefulWidget { /// /// This setting is only honored on iOS devices. /// - /// If unset, defaults to the brightness of - /// [ThemeData.brightness]. + /// If unset, defaults to the brightness of [ThemeData.brightness]. final Brightness? keyboardAppearance; /// {@macro flutter.widgets.editableText.scrollPadding} @@ -539,6 +528,11 @@ class StreamMessageTextField extends StatefulWidget { /// {@macro flutter.services.AutofillConfiguration.autofillHints} final Iterable? autofillHints; + /// {@macro flutter.material.Material.clipBehavior} + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + /// {@template flutter.material.textfield.restorationId} /// Restoration ID to save and restore the state of the text field. /// @@ -558,6 +552,9 @@ class StreamMessageTextField extends StatefulWidget { /// {@endtemplate} final String? restorationId; + /// {@macro flutter.widgets.editableText.scribbleEnabled} + final bool scribbleEnabled; + /// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning} final bool enableIMEPersonalizedLearning; @@ -567,6 +564,9 @@ class StreamMessageTextField extends StatefulWidget { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); + properties.add(DiagnosticsProperty( + 'controller', controller, + defaultValue: null)); properties.add(DiagnosticsProperty('focusNode', focusNode, defaultValue: null)); properties @@ -647,6 +647,10 @@ class StreamMessageTextField extends StatefulWidget { properties.add(DiagnosticsProperty( 'scrollPhysics', scrollPhysics, defaultValue: null)); + properties.add(DiagnosticsProperty('clipBehavior', clipBehavior, + defaultValue: Clip.hardEdge)); + properties.add(DiagnosticsProperty('scribbleEnabled', scribbleEnabled, + defaultValue: true)); properties.add(DiagnosticsProperty( 'enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true)); @@ -655,10 +659,9 @@ class StreamMessageTextField extends StatefulWidget { class _StreamMessageTextFieldState extends State with RestorationMixin { - StreamRestorableMessageInputController? _controller; - StreamMessageInputController get _effectiveController => widget.controller ?? _controller!.value; + StreamRestorableMessageInputController? _controller; @override void initState() { @@ -677,7 +680,7 @@ class _StreamMessageTextFieldState extends State void didUpdateWidget(covariant StreamMessageTextField oldWidget) { super.didUpdateWidget(oldWidget); if (widget.controller == null && oldWidget.controller != null) { - _createLocalController(oldWidget.controller!.value); + _createLocalController(oldWidget.controller!.message); } else if (widget.controller != null && oldWidget.controller == null) { unregisterFromRestoration(_controller!); _controller!.dispose(); @@ -697,19 +700,19 @@ class _StreamMessageTextFieldState extends State void _registerController() { assert(_controller != null, ''); - registerForRestoration(_controller!, restorationId ?? 'controller'); + registerForRestoration(_controller!, 'controller'); } @override Widget build(BuildContext context) => TextField( - controller: _effectiveController.textEditingController, - onChanged: (newText) { - _effectiveController.text = newText; - }, + controller: _effectiveController.textFieldController, focusNode: widget.focusNode, decoration: widget.decoration, keyboardType: widget.keyboardType, - textInputAction: widget.textInputAction, + textInputAction: widget.textInputAction ?? + (widget.keyboardType == TextInputType.multiline + ? TextInputAction.newline + : TextInputAction.send), textCapitalization: widget.textCapitalization, style: widget.style, strutStyle: widget.strutStyle, @@ -753,7 +756,9 @@ class _StreamMessageTextFieldState extends State scrollController: widget.scrollController, scrollPhysics: widget.scrollPhysics, autofillHints: widget.autofillHints, + clipBehavior: widget.clipBehavior, restorationId: widget.restorationId, + scribbleEnabled: widget.scribbleEnabled, enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, ); diff --git a/packages/stream_chat_flutter/lib/src/v4/message_input/tld.dart b/packages/stream_chat_flutter/lib/src/message_input/tld.dart similarity index 100% rename from packages/stream_chat_flutter/lib/src/v4/message_input/tld.dart rename to packages/stream_chat_flutter/lib/src/message_input/tld.dart diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/floating_date_divider.dart b/packages/stream_chat_flutter/lib/src/message_list_view/floating_date_divider.dart new file mode 100644 index 00000000..dbaccc17 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_list_view/floating_date_divider.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; +import 'package:stream_chat_flutter/src/message_list_view/mlv_utils.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template floatingDateDivider} +/// Not intended for use outside of [MessageListView]. +/// {@endtemplate} +class FloatingDateDivider extends StatelessWidget { + /// {@macro floatingDateDivider} + const FloatingDateDivider({ + super.key, + required this.itemPositionListener, + required this.reverse, + required this.messages, + required this.itemCount, + this.isThreadConversation = false, + this.dateDividerBuilder, + }); + + /// true if this is a thread conversation + final bool isThreadConversation; + + // ignore: public_member_api_docs + final ItemPositionsListener itemPositionListener; + + // ignore: public_member_api_docs + final bool reverse; + + // ignore: public_member_api_docs + final List messages; + + // ignore: public_member_api_docs + final int itemCount; + + // ignore: public_member_api_docs + final Widget Function(DateTime)? dateDividerBuilder; + + @override + Widget build(BuildContext context) { + return Positioned( + top: 20, + left: 0, + right: 0, + child: BetterStreamBuilder>( + initialData: itemPositionListener.itemPositions.value, + stream: valueListenableToStreamAdapter( + itemPositionListener.itemPositions, + ), + comparator: (a, b) { + if (a == null || b == null) { + return false; + } + if (reverse) { + final aTop = getTopElementIndex(a); + final bTop = getTopElementIndex(b); + return aTop == bTop; + } else { + final aBottom = getBottomElementIndex(a); + final bBottom = getBottomElementIndex(b); + return aBottom == bBottom; + } + }, + builder: (context, values) { + if (values.isEmpty || messages.isEmpty) { + return const Offstage(); + } + + int? index; + if (reverse) { + index = getTopElementIndex(values); + } else { + index = getBottomElementIndex(values); + } + + if ((index == null) || + (!isThreadConversation && index == itemCount - 2) || + (isThreadConversation && index == itemCount - 1)) { + return const Offstage(); + } + + if (index <= 2 || index >= itemCount - 3) { + if (reverse) { + index = itemCount - 4; + } else { + index = 2; + } + } + + final message = messages[index - 2]; + return dateDividerBuilder != null + ? dateDividerBuilder!(message.createdAt.toLocal()) + : StreamDateDivider(dateTime: message.createdAt.toLocal()); + }, + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/loading_indicator.dart b/packages/stream_chat_flutter/lib/src/message_list_view/loading_indicator.dart new file mode 100644 index 00000000..2d7aa7e7 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_list_view/loading_indicator.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template loadingIndicatorMLV} +/// A loading indicator for [MessageListView]. Not intended for use outside of +/// [MessageListView]. +/// {@endtemplate} +class LoadingIndicator extends StatelessWidget { + /// {@macro loadingIndicatorMLV} + const LoadingIndicator({ + super.key, + required this.streamTheme, + required this.isThreadConversation, + required this.direction, + required this.streamChannelState, + this.indicatorBuilder, + }); + + // ignore: public_member_api_docs + final StreamChatThemeData streamTheme; + + // ignore: public_member_api_docs + final bool isThreadConversation; + + // ignore: public_member_api_docs + final QueryDirection direction; + + // ignore: public_member_api_docs + final StreamChannelState streamChannelState; + + // ignore: public_member_api_docs + final WidgetBuilder? indicatorBuilder; + + @override + Widget build(BuildContext context) { + final stream = direction == QueryDirection.top + ? streamChannelState.queryTopMessages + : streamChannelState.queryBottomMessages; + return BetterStreamBuilder( + key: Key('LOADING-INDICATOR $direction'), + stream: stream, + initialData: false, + errorBuilder: (context, error) => ColoredBox( + color: streamTheme.colorTheme.accentError.withOpacity(0.2), + child: Center( + child: Text(context.translations.loadingMessagesError), + ), + ), + builder: (context, data) { + if (!data) return const Offstage(); + return indicatorBuilder?.call(context) ?? + const Center( + child: Padding( + padding: EdgeInsets.all(8), + child: CircularProgressIndicator(), + ), + ); + }, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/message_details.dart b/packages/stream_chat_flutter/lib/src/message_list_view/message_details.dart new file mode 100644 index 00000000..36ffd887 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_list_view/message_details.dart @@ -0,0 +1,36 @@ +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template messageDetails} +/// Class for message details +/// {@endtemplate} +// ignore: prefer-match-file-name +class MessageDetails { + /// {@macro messageDetails} + MessageDetails( + String currentUserId, + this.message, + List messages, + this.index, + ) { + isMyMessage = message.user?.id == currentUserId; + isLastUser = index + 1 < messages.length && + message.user?.id == messages[index + 1].user?.id; + isNextUser = + index - 1 >= 0 && message.user!.id == messages[index - 1].user?.id; + } + + /// True if the message belongs to the current user + late final bool isMyMessage; + + /// True if the user message is the same of the previous message + late final bool isLastUser; + + /// True if the user message is the same of the next message + late final bool isNextUser; + + /// The message + final Message message; + + /// The index of the message + final int index; +} diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart similarity index 71% rename from packages/stream_chat_flutter/lib/src/message_list_view.dart rename to packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart index af54d5ee..67d151ef 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart @@ -2,53 +2,18 @@ import 'dart:async'; import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_portal/flutter_portal.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/swipeable.dart'; +import 'package:stream_chat_flutter/src/message_list_view/floating_date_divider.dart'; +import 'package:stream_chat_flutter/src/message_list_view/loading_indicator.dart'; +import 'package:stream_chat_flutter/src/message_list_view/mlv_utils.dart'; +import 'package:stream_chat_flutter/src/message_list_view/thread_separator.dart'; +import 'package:stream_chat_flutter/src/message_list_view/unread_messages_separator.dart'; +import 'package:stream_chat_flutter/src/misc/swipeable.dart'; +import 'package:stream_chat_flutter/src/utils/utils.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// Widget builder for message -/// [defaultMessageWidget] is the default [StreamMessageWidget] configuration -/// Use [defaultMessageWidget.copyWith] to easily customize it -typedef MessageBuilder = Widget Function( - BuildContext, - MessageDetails, - List, - StreamMessageWidget defaultMessageWidget, -); - -/// Widget builder for parent message -/// [defaultMessageWidget] is the default [StreamMessageWidget] configuration -/// Use [defaultMessageWidget.copyWith] to easily customize it -typedef ParentMessageBuilder = Widget Function( - BuildContext, - Message?, - StreamMessageWidget defaultMessageWidget, -); - -/// Widget builder for system message -typedef SystemMessageBuilder = Widget Function( - BuildContext, - Message, -); - -/// Widget builder for thread -typedef ThreadBuilder = Widget Function(BuildContext context, Message? parent); - -/// Callback for thread taps -typedef ThreadTapCallback = void Function(Message, Widget?); - -/// Callback on message swiped -typedef OnMessageSwiped = void Function(Message); - -/// Callback on message tapped -typedef OnMessageTap = void Function(Message); - -/// Callback on reply tapped -typedef ReplyTapCallback = void Function(Message); - /// Spacing Types (These are properties of a message to help inform the decision /// of how much space / which widget to build after it) enum SpacingType { @@ -69,68 +34,11 @@ enum SpacingType { defaultSpacing, } -/// Builder for building certain spacing after widgets. -/// This spacing can be in form of any widgets you like. -/// A List of [SpacingType] is provided to help inform the decision of -/// what to build after the message. -/// -/// As an example: -/// MessageListView( -/// spacingWidgetBuilder: (context, list) { -/// if(list.contains(SpacingType.defaultSpacing)) { -/// return SizedBox(height: 2.0,); -/// } else { -/// return SizedBox(height: 8.0,); -/// } -/// }, -/// ), -typedef SpacingWidgetBuilder = Widget Function( - BuildContext context, - List spacingTypes, -); - -/// Class for message details -// ignore: prefer-match-file-name -class MessageDetails { - /// Constructor for creating [MessageDetails] - MessageDetails( - String currentUserId, - this.message, - List messages, - this.index, - ) { - isMyMessage = message.user?.id == currentUserId; - isLastUser = index + 1 < messages.length && - message.user?.id == messages[index + 1].user?.id; - isNextUser = - index - 1 >= 0 && message.user!.id == messages[index - 1].user?.id; - } - - /// True if the message belongs to the current user - late final bool isMyMessage; - - /// True if the user message is the same of the previous message - late final bool isLastUser; - - /// True if the user message is the same of the next message - late final bool isNextUser; - - /// The message - final Message message; - - /// The index of the message - final int index; -} - -/// {@macro message_list_view} -@Deprecated("Use 'StreamMessageListView' instead") -typedef MessageListView = StreamMessageListView; - -/// {@template message_list_view} +/// {@template streamMessageListView} /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_listview.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_listview_paint.png) /// -/// It shows the list of messages of the current channel. +/// Shows the list of messages in the current channel. /// /// ```dart /// class ChannelPage extends StatelessWidget { @@ -139,35 +47,38 @@ typedef MessageListView = StreamMessageListView; /// }) : super(key: key); /// /// @override -/// Widget build(BuildContext context) => Scaffold( -/// appBar: const StreamChannelHeader(), -/// body: Column( -/// children: [ -/// Expanded( -/// child: StreamMessageListView( -/// threadBuilder: (_, parentMessage) => ThreadPage( +/// Widget build(BuildContext context) { +/// return Scaffold( +/// appBar: StreamChannelHeader(), +/// body: Column( +/// children: [ +/// Expanded( +/// child: StreamMessageListView( +/// threadBuilder: (_, parentMessage) { +/// return ThreadPage( /// parent: parentMessage, -/// ), -/// ), +/// ); +/// }, /// ), -/// const StreamMessageInput(), -/// ], -/// ), -/// ); +/// ), +/// StreamMessageInput(), +/// ], +/// ), +/// ); +/// } /// } /// ``` /// +/// A [StreamChannel] ancestor widget is required in order to provide the +/// information about the channels. /// -/// Make sure to have a [StreamChannel] ancestor in order to -/// provide the information about the channels. -/// The widget uses a [ListView.custom] to render the list of channels. +/// Uses a [ListView.custom] to render the list of channels. /// -/// The widget components render the ui based on the first -/// ancestor of type [StreamChatTheme]. -/// Modify it to change the widget appearance. +/// The UI is rendered based on the first ancestor of type [StreamChatTheme]. +/// Modify it to change the widget's appearance. /// {@endtemplate} class StreamMessageListView extends StatefulWidget { - /// Instantiate a new StreamMessageListView. + /// {@macro streamMessageListView} const StreamMessageListView({ super.key, this.showScrollToBottom = true, @@ -213,7 +124,7 @@ class StreamMessageListView extends StatefulWidget { /// dismiss the keyboard automatically. final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; - /// Function used to build a custom message widget + /// {@macro messageBuilder} final MessageBuilder? messageBuilder; /// Whether the view scrolls in the reading direction. @@ -226,16 +137,17 @@ class StreamMessageListView extends StatefulWidget { /// Limit used during pagination final int paginationLimit; - /// Function used to build a custom system message widget + /// {@macro systemMessageBuilder} final SystemMessageBuilder? systemMessageBuilder; - /// Function used to build a custom parent message widget + /// {@macro parentMessageBuilder} final ParentMessageBuilder? parentMessageBuilder; - /// Function used to build a custom thread widget + /// {@macro threadBuilder} final ThreadBuilder? threadBuilder; - /// Function called when tapping on a thread + /// {@macro threadTapCallback} + /// /// By default it calls [Navigator.push] using the widget /// built using [threadBuilder] final ThreadTapCallback? onThreadTap; @@ -250,14 +162,16 @@ class StreamMessageListView extends StatefulWidget { /// to the function that is executed on tap of this widget by default /// /// As an example: - /// MessageListView( - /// scrollToBottomBuilder: (unreadCount, defaultTapAction) { - /// return InkWell( - /// onTap: () => defaultTapAction(unreadCount), - /// child: Text('Scroll To Bottom'), - /// ); - /// }, - /// ), + /// ``` + /// MessageListView( + /// scrollToBottomBuilder: (unreadCount, defaultTapAction) { + /// return InkWell( + /// onTap: () => defaultTapAction(unreadCount), + /// child: Text('Scroll To Bottom'), + /// ); + /// }, + /// ), + /// ``` final Widget Function( int unreadCount, Future Function(int) scrollToBottomDefaultTapAction, @@ -286,7 +200,7 @@ class StreamMessageListView extends StatefulWidget { /// The ScrollPhysics used by the ListView final ScrollPhysics? scrollPhysics; - /// Called when message item gets swiped + /// {@macro onMessageSwiped} final OnMessageSwiped? onMessageSwiped; /// If true the list will highlight the initialMessage if there is any. @@ -320,9 +234,9 @@ class StreamMessageListView extends StatefulWidget { /// Callback triggered when an error occurs while performing the /// given request. + /// /// This parameter can be used to display an error message to - /// users in the event - /// of a connection failure. + /// users in the event of a connection failure. final ErrorBuilder? errorBuilder; /// Predicate used to filter messages @@ -336,23 +250,22 @@ class StreamMessageListView extends StatefulWidget { final OnMessageTap? onSystemMessageTap; /// Builder used to build the thread separator in case it's a thread view - final WidgetBuilder? threadSeparatorBuilder; + final Function(BuildContext context, Message parentMessage)? + threadSeparatorBuilder; /// Builder used to build the unread message separator final Widget Function(BuildContext context, int unreadCount)? unreadMessagesSeparatorBuilder; /// A [MessageListController] allows pagination. + /// /// Use [ChannelListController.paginateData] pagination. final MessageListController? messageListController; /// Builder used to build the loading indicator shown while paginating. final WidgetBuilder? paginationLoadingIndicatorBuilder; - /// This allows a user to customise the space after a message - /// A List of [SpacingType] is provided to provide more data about the - /// type of message (thread, difference in time between current and last - /// message, default spacing, etc) + /// {@macro spacingWidgetBuilder} final SpacingWidgetBuilder spacingWidgetBuilder; static Widget _defaultSpacingWidgetBuilder( @@ -380,39 +293,12 @@ class _StreamMessageListViewState extends State { late List _userPermissions; late int unreadCount; - int get _initialIndex { - final initialScrollIndex = widget.initialScrollIndex; - if (initialScrollIndex != null) return initialScrollIndex; - if (streamChannel!.initialMessageId != null) { - final messages = streamChannel!.channel.state!.messages - .where(widget.messageFilter ?? - defaultMessageFilter( - streamChannel!.channel.client.state.currentUser!.id, - )) - .toList(growable: false); - final totalMessages = messages.length; - final messageIndex = - messages.indexWhere((e) => e.id == streamChannel!.initialMessageId); - final index = totalMessages - messageIndex; - if (index != 0) return index + 1; - return index; - } - - if (unreadCount > 0) { - return unreadCount + 1; - } - - return 0; - } - double get _initialAlignment { final initialAlignment = widget.initialAlignment; if (initialAlignment != null) return initialAlignment; - return streamChannel!.initialMessageId == null ? 0 : 0.1; + return initialIndex == 0 ? 0 : 0.1; } - bool _isInitialMessage(String id) => streamChannel!.initialMessageId == id; - bool get _upToDate => streamChannel!.channel.state!.isUpToDate; bool get _isThreadConversation => widget.parentMessage != null; @@ -435,8 +321,98 @@ class _StreamMessageListViewState extends State { MessageListController get _messageListController => widget.messageListController ?? _defaultController; + StreamSubscription? _messageNewListener; + + Read? _userRead; + Message? _oldestUnreadMessage; + @override - Widget build(BuildContext context) => MessageListCore( + void initState() { + super.initState(); + + _scrollController = widget.scrollController ?? ItemScrollController(); + _itemPositionListener = + widget.itemPositionListener ?? ItemPositionsListener.create(); + _itemPositionListener.itemPositions + .addListener(_handleItemPositionsChanged); + + _getOnThreadTap(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final newStreamChannel = StreamChannel.of(context); + _streamTheme = StreamChatTheme.of(context); + _userPermissions = newStreamChannel.channel.ownCapabilities; + + if (newStreamChannel != streamChannel) { + streamChannel = newStreamChannel; + + _userRead = streamChannel?.channel.state!.read.firstWhereOrNull( + (it) => + it.user.id == streamChannel?.channel.client.state.currentUser?.id, + ); + _messageNewListener?.cancel(); + unreadCount = streamChannel?.channel.state?.unreadCount ?? 0; + initialIndex = getInitialIndex( + widget.initialScrollIndex, + streamChannel!, + widget.messageFilter, + _userRead, + ); + + initialAlignment = _initialAlignment; + + if (_scrollController?.isAttached == true) { + _scrollController?.jumpTo( + index: initialIndex, + alignment: initialAlignment, + ); + } + + _messageNewListener = + streamChannel!.channel.on(EventType.messageNew).listen((event) { + if (_upToDate) { + _bottomPaginationActive = false; + } + if (event.message?.parentId == widget.parentMessage?.id && + event.message!.user!.id == + streamChannel!.channel.client.state.currentUser!.id) { + setState(() => unreadCount = 0); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollController?.jumpTo( + index: 0, + ); + }); + } + }); + + if (_isThreadConversation) { + streamChannel!.getReplies(widget.parentMessage!.id); + } + + unreadCount = streamChannel?.channel.state?.unreadCount ?? 0; + } + } + + @override + void dispose() { + if (!_upToDate) { + streamChannel!.reloadChannel(); + } + _messageNewListener?.cancel(); + _itemPositionListener.itemPositions + .removeListener(_handleItemPositionsChanged); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Portal( + labels: const [kPortalMessageListViewLabel], + child: MessageListCore( paginationLimit: widget.paginationLimit, messageFilter: widget.messageFilter, loadingBuilder: widget.loadingBuilder ?? @@ -467,10 +443,25 @@ class _StreamMessageListViewState extends State { ), ), ), - ); + ), + ); + } Widget _buildListView(List data) { messages = data; + + if (_userRead != null && + messages.isNotEmpty && + messages.first.createdAt.isAfter(_userRead!.lastRead) && + messages.last.createdAt.isBefore(_userRead!.lastRead)) { + _oldestUnreadMessage = messages.lastWhereOrNull( + (it) => + it.user?.id != + streamChannel?.channel.client.state.currentUser?.id && + it.createdAt.compareTo(_userRead!.lastRead) > 0, + ); + } + for (var index = 0; index < messages.length; index++) { messagesIndex[messages[index].id] = index; } @@ -594,7 +585,15 @@ class _StreamMessageListViewState extends State { if (widget.parentMessage == null) { return const Offstage(); } - return _buildThreadSeparator(); + + if (widget.threadSeparatorBuilder != null) { + return widget.threadSeparatorBuilder! + .call(context, widget.parentMessage!); + } + + return ThreadSeparator( + parentMessage: widget.parentMessage, + ); } if (i == itemCount - 3) { if (widget.reverse @@ -617,9 +616,7 @@ class _StreamMessageListViewState extends State { return const SizedBox(height: 8); } - if (i == 1 || i == itemCount - 4) { - return const Offstage(); - } + if (i == 1 || i == itemCount - 4) return const Offstage(); late final Message message, nextMessage; if (widget.reverse) { @@ -668,7 +665,9 @@ class _StreamMessageListViewState extends State { ); } - if (!isThread && unreadCount > 0 && unreadCount == i - 1) { + if (!isThread && + unreadCount > 0 && + _oldestUnreadMessage?.id == nextMessage.id) { final unreadMessagesSeparator = _buildUnreadMessagesSeparator(unreadCount); @@ -680,7 +679,6 @@ class _StreamMessageListViewState extends State { ], ); } - return separator; }, itemBuilder: (context, i) { @@ -705,17 +703,21 @@ class _StreamMessageListViewState extends State { widget.paginationLoadingIndicatorBuilder; if (i == itemCount - 3) { - return _loadingIndicator( - streamChannel!, - QueryDirection.top, + return LoadingIndicator( + direction: QueryDirection.top, + streamTheme: _streamTheme, + streamChannelState: streamChannel!, + isThreadConversation: _isThreadConversation, indicatorBuilder: indicatorBuilder, ); } if (i == 1) { - return _loadingIndicator( - streamChannel!, - QueryDirection.bottom, + return LoadingIndicator( + direction: QueryDirection.bottom, + streamTheme: _streamTheme, + streamChannelState: streamChannel!, + isThreadConversation: _isThreadConversation, indicatorBuilder: indicatorBuilder, ); } @@ -772,7 +774,14 @@ class _StreamMessageListViewState extends State { ), ), if (widget.showFloatingDateDivider) - _buildFloatingDateDivider(itemCount), + FloatingDateDivider( + itemCount: itemCount, + reverse: widget.reverse, + itemPositionListener: _itemPositionListener, + messages: messages, + dateDividerBuilder: widget.dateDividerBuilder, + isThreadConversation: _isThreadConversation, + ), ], ); @@ -797,143 +806,16 @@ class _StreamMessageListViewState extends State { Widget _buildUnreadMessagesSeparator(int unreadCount) { final unreadMessagesSeparator = widget.unreadMessagesSeparatorBuilder?.call(context, unreadCount) ?? - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: DecoratedBox( - decoration: BoxDecoration( - gradient: _streamTheme.colorTheme.bgGradient, - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: Text( - context.translations.unreadMessagesSeparatorText( - unreadCount, - ), - textAlign: TextAlign.center, - style: StreamChannelHeaderTheme.of(context).subtitleStyle, - ), - ), - ), - ); + UnreadMessagesSeparator(unreadCount: unreadCount); return unreadMessagesSeparator; } - Widget _buildDateDivider(Message message) { - final divider = widget.dateDividerBuilder != null - ? widget.dateDividerBuilder!( - message.createdAt.toLocal(), - ) - : Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: StreamDateDivider( - dateTime: message.createdAt.toLocal(), - ), - ); - return divider; - } - - Widget _buildThreadSeparator() { - if (widget.threadSeparatorBuilder != null) { - return widget.threadSeparatorBuilder!.call(context); - } - - final replyCount = widget.parentMessage!.replyCount!; - return DecoratedBox( - decoration: BoxDecoration( - gradient: _streamTheme.colorTheme.bgGradient, - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: Text( - context.translations.threadSeparatorText(replyCount), - textAlign: TextAlign.center, - style: StreamChannelHeaderTheme.of(context).subtitleStyle, - ), - ), - ); - } - - Positioned _buildFloatingDateDivider(int itemCount) => Positioned( - top: 20, - left: 0, - right: 0, - child: BetterStreamBuilder>( - initialData: _itemPositionListener.itemPositions.value, - stream: _valueListenableToStreamAdapter( - _itemPositionListener.itemPositions, - ), - comparator: (a, b) { - if (a == null || b == null) { - return false; - } - if (widget.reverse) { - final aTop = _getTopElementIndex(a); - final bTop = _getTopElementIndex(b); - return aTop == bTop; - } else { - final aBottom = _getBottomElementIndex(a); - final bBottom = _getBottomElementIndex(b); - return aBottom == bBottom; - } - }, - builder: (context, values) { - if (values.isEmpty || messages.isEmpty) { - return const Offstage(); - } - - int? index; - if (widget.reverse) { - index = _getTopElementIndex(values); - } else { - index = _getBottomElementIndex(values); - } - - if ((index == null) || - (!_isThreadConversation && index == itemCount - 2) || - (_isThreadConversation && index == itemCount - 1)) { - return const Offstage(); - } - - if (index <= 2 || index >= itemCount - 3) { - if (widget.reverse) { - index = itemCount - 4; - } else { - index = 2; - } - } - - final message = messages[index - 2]; - return widget.dateDividerBuilder != null - ? widget.dateDividerBuilder!(message.createdAt.toLocal()) - : StreamDateDivider(dateTime: message.createdAt.toLocal()); - }, - ), - ); - Future _paginateData( StreamChannelState? channel, QueryDirection direction, ) => _messageListController.paginateData!(direction: direction); - int? _getTopElementIndex(Iterable values) { - final inView = values.where((position) => position.itemLeadingEdge < 1); - if (inView.isEmpty) return null; - return inView - .reduce((max, position) => - position.itemLeadingEdge > max.itemLeadingEdge ? position : max) - .index; - } - - int? _getBottomElementIndex(Iterable values) { - final inView = values.where((position) => position.itemLeadingEdge < 1); - if (inView.isEmpty) return null; - return inView - .reduce((min, position) => - position.itemLeadingEdge < min.itemLeadingEdge ? position : min) - .index; - } - Future scrollToBottomDefaultTapAction(int unreadCount) async { this.unreadCount = unreadCount; if (unreadCount > 0) { @@ -960,81 +842,19 @@ class _StreamMessageListViewState extends State { } } - Widget _buildScrollToBottom() => StreamBuilder( - stream: streamChannel!.channel.state!.unreadCountStream, - builder: (_, snapshot) { - if (snapshot.hasError) { - return const Offstage(); - } else if (!snapshot.hasData) { - return const Offstage(); - } - final unreadCount = snapshot.data!; - if (widget.scrollToBottomBuilder != null) { - return widget.scrollToBottomBuilder!( - unreadCount, - scrollToBottomDefaultTapAction, - ); - } - final showUnreadCount = unreadCount > 0 && - streamChannel!.channel.state!.members.any((e) => - e.userId == - streamChannel!.channel.client.state.currentUser!.id); - return Positioned( - bottom: 8, - right: 8, - width: 40, - height: 40, - child: Stack( - clipBehavior: Clip.none, - children: [ - FloatingActionButton( - backgroundColor: _streamTheme.colorTheme.barsBg, - onPressed: () => scrollToBottomDefaultTapAction(unreadCount), - child: widget.reverse - ? StreamSvgIcon.down( - color: _streamTheme.colorTheme.textHighEmphasis, - ) - : StreamSvgIcon.up( - color: _streamTheme.colorTheme.textHighEmphasis, - ), - ), - if (showUnreadCount) - Positioned( - width: 20, - height: 20, - left: 10, - top: -10, - child: CircleAvatar( - child: Padding( - padding: const EdgeInsets.all(3), - child: Text( - '$unreadCount', - style: const TextStyle( - fontSize: 11, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ), - ], + Widget _buildDateDivider(Message message) { + final divider = widget.dateDividerBuilder != null + ? widget.dateDividerBuilder!( + message.createdAt.toLocal(), + ) + : Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: StreamDateDivider( + dateTime: message.createdAt.toLocal(), ), ); - }, - ); - - Widget _loadingIndicator( - StreamChannelState streamChannel, - QueryDirection direction, { - WidgetBuilder? indicatorBuilder, - }) => - _LoadingIndicator( - direction: direction, - streamTheme: _streamTheme, - streamChannel: streamChannel, - isThreadConversation: _isThreadConversation, - indicatorBuilder: indicatorBuilder, - ); + return divider; + } Widget _buildBottomMessage( BuildContext context, @@ -1087,16 +907,6 @@ class _StreamMessageListViewState extends State { messageTheme: isMyMessage ? _streamTheme.ownMessageTheme : _streamTheme.otherMessageTheme, - onReturnAction: (action) { - switch (action) { - case ReturnActionType.none: - break; - case ReturnActionType.reply: - FocusScope.of(context).unfocus(); - widget.onMessageSwiped?.call(message); - break; - } - }, onMessageTap: (message) { widget.onMessageTap?.call(message); FocusScope.of(context).unfocus(); @@ -1116,6 +926,99 @@ class _StreamMessageListViewState extends State { return defaultMessageWidget; } + Widget _buildScrollToBottom() { + return StreamBuilder( + stream: streamChannel!.channel.state!.unreadCountStream, + builder: (_, snapshot) { + if (snapshot.hasError) { + return const Offstage(); + } else if (!snapshot.hasData) { + return const Offstage(); + } + final unreadCount = snapshot.data!; + if (widget.scrollToBottomBuilder != null) { + return widget.scrollToBottomBuilder!( + unreadCount, + scrollToBottomDefaultTapAction, + ); + } + final showUnreadCount = unreadCount > 0 && + streamChannel!.channel.state!.members.any((e) => + e.userId == + streamChannel!.channel.client.state.currentUser!.id); + return Positioned( + bottom: 8, + right: 8, + width: 40, + height: 40, + child: Stack( + clipBehavior: Clip.none, + children: [ + FloatingActionButton( + backgroundColor: _streamTheme.colorTheme.barsBg, + onPressed: () async { + if (unreadCount > 0) { + streamChannel!.channel.markRead(); + } + if (!_upToDate) { + _bottomPaginationActive = false; + initialAlignment = 0; + initialIndex = 0; + await streamChannel!.reloadChannel(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollController!.jumpTo(index: 0); + }); + } else { + _showScrollToBottom.value = false; + _scrollController!.jumpTo( + index: 0, + ); + } + }, + child: widget.reverse + ? StreamSvgIcon.down( + color: _streamTheme.colorTheme.textHighEmphasis, + ) + : StreamSvgIcon.up( + color: _streamTheme.colorTheme.textHighEmphasis, + ), + ), + if (showUnreadCount) + Positioned( + left: 0, + right: 0, + top: -10, + child: Center( + child: Material( + borderRadius: BorderRadius.circular(8), + color: + StreamChatTheme.of(context).colorTheme.accentPrimary, + child: Padding( + padding: const EdgeInsets.only( + left: 5, + right: 5, + top: 2, + bottom: 2, + ), + child: Text( + '${unreadCount > 99 ? '99+' : unreadCount}', + style: const TextStyle( + fontSize: 11, + color: Colors.white, + ), + ), + ), + ), + ), + ), + ], + ), + ); + }, + ); + } + Widget buildMessage(Message message, List messages, int index) { if ((message.type == 'system' || message.type == 'error') && message.text?.isNotEmpty == true) { @@ -1274,16 +1177,6 @@ class _StreamMessageListViewState extends State { messageTheme: isMyMessage ? _streamTheme.ownMessageTheme : _streamTheme.otherMessageTheme, - onReturnAction: (action) { - switch (action) { - case ReturnActionType.none: - break; - case ReturnActionType.reply: - FocusScope.of(context).unfocus(); - widget.onMessageSwiped?.call(message); - break; - } - }, onMessageTap: (message) { widget.onMessageTap?.call(message); FocusScope.of(context).unfocus(); @@ -1329,7 +1222,7 @@ class _StreamMessageListViewState extends State { if (!initialMessageHighlightComplete && widget.highlightInitialMessage && - _isInitialMessage(message.id)) { + isInitialMessage(message.id, streamChannel)) { final colorTheme = _streamTheme.colorTheme; final highlightColor = widget.messageHighlightColor ?? colorTheme.highlight; @@ -1353,79 +1246,6 @@ class _StreamMessageListViewState extends State { return child; } - StreamSubscription? _messageNewListener; - - @override - void initState() { - _scrollController = widget.scrollController ?? ItemScrollController(); - _itemPositionListener = - widget.itemPositionListener ?? ItemPositionsListener.create(); - _itemPositionListener.itemPositions - .addListener(_handleItemPositionsChanged); - - _getOnThreadTap(); - super.initState(); - } - - @override - void didChangeDependencies() { - final newStreamChannel = StreamChannel.of(context); - _streamTheme = StreamChatTheme.of(context); - _userPermissions = newStreamChannel.channel.ownCapabilities; - - if (newStreamChannel != streamChannel) { - streamChannel = newStreamChannel; - _messageNewListener?.cancel(); - - unreadCount = streamChannel?.channel.state?.unreadCount ?? 0; - initialIndex = _initialIndex; - initialAlignment = _initialAlignment; - - if (_scrollController?.isAttached == true) { - _scrollController?.jumpTo( - index: initialIndex, - alignment: initialAlignment, - ); - } - - _messageNewListener = - streamChannel!.channel.on(EventType.messageNew).skip(1) - //skipping the first event because - //the StreamController is a BehaviorSubject - .listen((event) { - if (_upToDate) { - _bottomPaginationActive = false; - } - if (event.message?.parentId == widget.parentMessage?.id && - event.message!.user!.id == - streamChannel!.channel.client.state.currentUser!.id) { - setState(() { - unreadCount = 0; - }); - - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollController?.scrollTo( - index: 0, - duration: const Duration(seconds: 1), - ); - }); - } else if (streamChannel?.channel.state?.unreadCount != 0) { - setState(() { - unreadCount = unreadCount + 1; - }); - } - }); - - if (_isThreadConversation) { - streamChannel!.getReplies(widget.parentMessage!.id); - } - - unreadCount = streamChannel?.channel.state?.unreadCount ?? 0; - } - - super.didChangeDependencies(); - } - void _handleItemPositionsChanged() { final _itemPositions = _itemPositionListener.itemPositions.value.toList(); final _firstItemIndex = @@ -1465,8 +1285,7 @@ class _StreamMessageListViewState extends State { }; } else if (widget.threadBuilder != null) { _onThreadTap = (Message message) { - Navigator.push( - context, + Navigator.of(context).push( MaterialPageRoute( builder: (_) => BetterStreamBuilder( stream: streamChannel!.channel.state!.messagesStream.map( @@ -1483,85 +1302,4 @@ class _StreamMessageListViewState extends State { }; } } - - @override - void dispose() { - if (!_upToDate) { - streamChannel!.reloadChannel(); - } - _messageNewListener?.cancel(); - _itemPositionListener.itemPositions - .removeListener(_handleItemPositionsChanged); - super.dispose(); - } -} - -class _LoadingIndicator extends StatelessWidget { - const _LoadingIndicator({ - required this.streamTheme, - required this.isThreadConversation, - required this.direction, - required this.streamChannel, - this.indicatorBuilder, - }); - - final StreamChatThemeData streamTheme; - final bool isThreadConversation; - final QueryDirection direction; - final StreamChannelState streamChannel; - final WidgetBuilder? indicatorBuilder; - - @override - Widget build(BuildContext context) { - final stream = direction == QueryDirection.top - ? streamChannel.queryTopMessages - : streamChannel.queryBottomMessages; - return BetterStreamBuilder( - key: Key('LOADING-INDICATOR $direction'), - stream: stream, - initialData: false, - errorBuilder: (context, error) => ColoredBox( - color: streamTheme.colorTheme.accentError.withOpacity(0.2), - child: Center( - child: Text(context.translations.loadingMessagesError), - ), - ), - builder: (context, data) { - if (!data) return const Offstage(); - return indicatorBuilder?.call(context) ?? - const Center( - child: Padding( - padding: EdgeInsets.all(8), - child: CircularProgressIndicator(), - ), - ); - }, - ); - } -} - -Stream _valueListenableToStreamAdapter(ValueListenable listenable) { - // ignore: close_sinks - late StreamController _controller; - - void listener() { - _controller.add(listenable.value); - } - - void start() { - listenable.addListener(listener); - } - - void end() { - listenable.removeListener(listener); - } - - _controller = StreamController( - onListen: start, - onPause: end, - onResume: start, - onCancel: end, - ); - - return _controller.stream; } diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/mlv_utils.dart b/packages/stream_chat_flutter/lib/src/message_list_view/mlv_utils.dart new file mode 100644 index 00000000..f926eff0 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_list_view/mlv_utils.dart @@ -0,0 +1,103 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// Determines at which point in the [MessageListView] the initial index should +/// be. +int getInitialIndex( + int? initialScrollIndex, + StreamChannelState channelState, + bool Function(Message)? messageFilter, + Read? read, +) { + if (initialScrollIndex != null) { + return initialScrollIndex; + } + + final messages = channelState.channel.state!.messages + .where(messageFilter ?? + defaultMessageFilter( + channelState.channel.client.state.currentUser!.id, + )) + .toList(growable: false); + + if (channelState.initialMessageId != null) { + final totalMessages = messages.length; + final messageIndex = + messages.indexWhere((e) => e.id == channelState.initialMessageId); + final index = totalMessages - messageIndex; + if (index != 0) return index + 1; + return index; + } + + if (read != null) { + final oldestUnreadMessage = messages.firstWhereOrNull( + (it) => + it.user?.id != channelState.channel.client.state.currentUser?.id && + it.createdAt.compareTo(read.lastRead) > 0, + ); + + if (oldestUnreadMessage != null) { + final oldestUnreadMessageIndex = messages.indexOf(oldestUnreadMessage); + final index = messages.length - oldestUnreadMessageIndex; + return index + 1; + } + } + + return 0; +} + +/// Gets the index of the top element in the viewport. +int? getTopElementIndex(Iterable values) { + final inView = values.where((position) => position.itemLeadingEdge < 1); + if (inView.isEmpty) return null; + return inView + .reduce((max, position) => + position.itemLeadingEdge > max.itemLeadingEdge ? position : max) + .index; +} + +/// Gets the index of the bottom element in the viewport. +int? getBottomElementIndex(Iterable values) { + final inView = values.where((position) => position.itemLeadingEdge < 1); + if (inView.isEmpty) return null; + return inView + .reduce((min, position) => + position.itemLeadingEdge < min.itemLeadingEdge ? position : min) + .index; +} + +/// Returns true if the message is the initial message. +bool isInitialMessage(String id, StreamChannelState? channelState) { + return channelState!.initialMessageId == id; +} + +/// Converts a [ValueListenable] to a [Stream]. +Stream valueListenableToStreamAdapter(ValueListenable listenable) { + // ignore: close_sinks + late StreamController _controller; + + void listener() { + _controller.add(listenable.value); + } + + void start() { + listenable.addListener(listener); + } + + void end() { + listenable.removeListener(listener); + } + + _controller = StreamController( + onListen: start, + onPause: end, + onResume: start, + onCancel: end, + ); + + return _controller.stream; +} diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/thread_separator.dart b/packages/stream_chat_flutter/lib/src/message_list_view/thread_separator.dart new file mode 100644 index 00000000..f988400b --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_list_view/thread_separator.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template threadSeparator} +/// A widget that separates messages in a thread. Not intended for use outside +/// of [StreamMessageWidget]. +/// {@endtemplate} +class ThreadSeparator extends StatelessWidget { + ///{@macro threadSeparator} + const ThreadSeparator({ + super.key, + this.parentMessage, + }); + + // ignore: public_member_api_docs + final Message? parentMessage; + + @override + Widget build(BuildContext context) { + final replyCount = parentMessage!.replyCount!; + return DecoratedBox( + decoration: BoxDecoration( + gradient: StreamChatTheme.of(context).colorTheme.bgGradient, + ), + child: Padding( + padding: const EdgeInsets.all(8), + child: Text( + context.translations.threadSeparatorText(replyCount), + textAlign: TextAlign.center, + style: StreamChannelHeaderTheme.of(context).subtitleStyle, + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/unread_messages_separator.dart b/packages/stream_chat_flutter/lib/src/message_list_view/unread_messages_separator.dart new file mode 100644 index 00000000..8d3ce0ee --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_list_view/unread_messages_separator.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template unreadMessagesSeparator} +/// {@endtemplate} +class UnreadMessagesSeparator extends StatelessWidget { + /// {@macro unreadMessagesSeparator} + const UnreadMessagesSeparator({ + super.key, + required this.unreadCount, + }); + + /// Number of unread messages. + final int unreadCount; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: DecoratedBox( + decoration: BoxDecoration( + gradient: StreamChatTheme.of(context).colorTheme.bgGradient, + ), + child: Padding( + padding: const EdgeInsets.all(8), + child: Text( + context.translations.unreadMessagesSeparatorText( + unreadCount, + ), + textAlign: TextAlign.center, + style: StreamChannelHeaderTheme.of(context).subtitleStyle, + ), + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_search_item.dart b/packages/stream_chat_flutter/lib/src/message_search_item.dart deleted file mode 100644 index 824c96a1..00000000 --- a/packages/stream_chat_flutter/lib/src/message_search_item.dart +++ /dev/null @@ -1,183 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template message_search_item} -/// It shows the current [Message] preview. -/// -/// Usually you don't use this widget as it's the default item used by -/// [MessageSearchListView]. -/// -/// The widget renders the ui based on the first ancestor of type -/// [StreamChatTheme]. -/// Modify it to change the widget appearance. -/// {@endtemplate} -@Deprecated("Use 'StreamMessageSearchItem' instead") -class MessageSearchItem extends StatelessWidget { - /// Instantiate a new MessageSearchItem - const MessageSearchItem({ - super.key, - required this.getMessageResponse, - this.onTap, - this.showOnlineStatus = true, - }); - - /// [Message] displayed - final GetMessageResponse getMessageResponse; - - /// Function called when tapping this widget - final VoidCallback? onTap; - - /// If true the [MessageSearchItem] will show the current online Status - final bool showOnlineStatus; - - @override - Widget build(BuildContext context) { - final message = getMessageResponse.message; - final channel = getMessageResponse.channel; - final channelName = channel?.extraData['name']; - final user = message.user!; - final channelPreviewTheme = StreamChannelPreviewTheme.of(context); - return ListTile( - onTap: onTap, - leading: StreamUserAvatar( - user: user, - showOnlineStatus: showOnlineStatus, - constraints: const BoxConstraints.tightFor( - height: 40, - width: 40, - ), - ), - title: Row( - children: [ - Text( - user.id == StreamChat.of(context).currentUser?.id - ? context.translations.youText - : user.name, - style: channelPreviewTheme.titleStyle, - ), - if (channelName != null) ...[ - Text( - ' ${context.translations.inText} ', - style: channelPreviewTheme.titleStyle?.copyWith( - fontWeight: FontWeight.normal, - ), - ), - Text( - channelName as String, - style: channelPreviewTheme.titleStyle, - ), - ], - ], - ), - subtitle: Row( - children: [ - Expanded(child: _buildSubtitle(context, message)), - const SizedBox(width: 16), - _buildDate(context, message), - ], - ), - ); - } - - Widget _buildDate(BuildContext context, Message message) { - final createdAt = message.createdAt; - String stringDate; - final now = DateTime.now(); - - if (now.year != createdAt.year || - now.month != createdAt.month || - now.day != createdAt.day) { - stringDate = Jiffy(createdAt.toLocal()).yMd; - } else { - stringDate = Jiffy(createdAt.toLocal()).jm; - } - - return Text( - stringDate, - style: StreamChatTheme.of(context).channelPreviewTheme.lastMessageAtStyle, - ); - } - - Widget _buildSubtitle(BuildContext context, Message message) { - var text = message.text; - if (message.isDeleted) { - text = context.translations.messageDeletedText; - } else if (message.attachments.isNotEmpty) { - final parts = [ - ...message.attachments.map((e) { - if (e.type == 'image') { - return '📷'; - } else if (e.type == 'video') { - return '🎬'; - } else if (e.type == 'giphy') { - return '[GIF]'; - } - return e == message.attachments.last - ? (e.title ?? 'File') - : '${e.title ?? 'File'} , '; - }), - message.text ?? '', - ]; - - text = parts.join(' '); - } - - final channelPreviewTheme = StreamChannelPreviewTheme.of(context); - return Text.rich( - _getDisplayText( - text!, - message.mentionedUsers, - message.attachments, - channelPreviewTheme.subtitleStyle?.copyWith( - fontStyle: (message.isSystem || message.isDeleted) - ? FontStyle.italic - : FontStyle.normal, - ), - channelPreviewTheme.subtitleStyle?.copyWith( - fontStyle: (message.isSystem || message.isDeleted) - ? FontStyle.italic - : FontStyle.normal, - fontWeight: FontWeight.bold, - ), - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ); - } - - TextSpan _getDisplayText( - String text, - List mentions, - List attachments, - TextStyle? normalTextStyle, - TextStyle? mentionsTextStyle, - ) { - final textList = text.split(' '); - final resList = []; - for (final e in textList) { - if (mentions.isNotEmpty && - mentions.any((element) => '@${element.name}' == e)) { - resList.add(TextSpan( - text: '$e ', - style: mentionsTextStyle, - )); - } else if (attachments.isNotEmpty && - attachments - .where((e) => e.title != null) - .any((element) => element.title == e)) { - resList.add(TextSpan( - text: '$e ', - style: normalTextStyle?.copyWith(fontStyle: FontStyle.italic), - )); - } else { - resList.add(TextSpan( - text: e == textList.last ? e : '$e ', - style: normalTextStyle, - )); - } - } - - return TextSpan(children: resList); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart deleted file mode 100644 index 665b23d0..00000000 --- a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart +++ /dev/null @@ -1,327 +0,0 @@ -// ignore: lines_longer_than_80_chars -// ignore_for_file: deprecated_member_use_from_same_package, deprecated_member_use - -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// Callback called when tapping on a user -typedef MessageSearchItemTapCallback = void Function(GetMessageResponse); - -/// Builder used to create a custom [ListUserItem] from a [User] -typedef MessageSearchItemBuilder = Widget Function( - BuildContext, - GetMessageResponse, -); - -/// Builder used when [MessageSearchListView] is empty -typedef EmptyMessageSearchBuilder = Widget Function( - BuildContext context, - String searchQuery, -); - -/// {@template message_search_list_view} -/// It shows the list of searched messages. -/// -/// ```dart -/// class MessageSearchPage extends StatelessWidget { -/// @override -/// Widget build(BuildContext context) { -/// return Scaffold( -/// body: MessageSearchListView( -/// messageQuery: _channelQuery, -/// filters: { -/// 'members': { -/// r'$in': [user.id] -/// } -/// }, -/// limit: 20, -/// ), -/// ); -/// } -/// } -/// ``` -/// -/// -/// Make sure to have a [MessageSearchBloc] ancestor in order to provide the -/// information about the messages. -/// The widget uses a [ListView.separated] to render the list of messages. -/// -/// The widget components render the ui based on the first ancestor of type -/// [StreamChatTheme]. -/// Modify it to change the widget appearance. -/// {@endtemplate} -@Deprecated("Use 'StreamMessageSearchListView' instead") -class MessageSearchListView extends StatefulWidget { - /// Instantiate a new MessageSearchListView - @Deprecated("Use 'StreamMessageSearchListView' instead") - const MessageSearchListView({ - super.key, - required this.filters, - this.messageQuery, - this.sortOptions, - this.limit = 30, - this.messageFilters, - this.separatorBuilder, - this.itemBuilder, - this.onItemTap, - this.showResultCount = true, - this.pullToRefresh = true, - this.showErrorTile = false, - this.emptyBuilder, - this.errorBuilder, - this.loadingBuilder, - this.childBuilder, - this.messageSearchListController, - }); - - /// Message String to search on - final String? messageQuery; - - /// The query filters to use. - /// You can query on any of the custom fields you've defined on the [Channel]. - /// You can also filter other built-in channel fields. - final Filter filters; - - /// The sorting used for the channels matching the filters. - /// Sorting is based on field and direction, multiple sorting options - /// can be provided. - /// You can sort based on last_updated, last_message_at, updated_at, - /// created_at or member_count. - /// Direction can be ascending or descending. - final List? sortOptions; - - /// The amount of messages requested per API call. - final int limit; - - /// The message query filters to use. - /// You can query on any of the custom fields you've defined on the [Channel]. - /// You can also filter other built-in channel fields. - final Filter? messageFilters; - - /// Builder used to create a custom item preview - final MessageSearchItemBuilder? itemBuilder; - - /// Function called when tapping on a [MessageSearchItem] - final MessageSearchItemTapCallback? onItemTap; - - /// Builder used to create a custom item separator - final IndexedWidgetBuilder? separatorBuilder; - - /// Set it to false to hide total results text - final bool showResultCount; - - /// Set it to false to disable the pull-to-refresh widget - final bool pullToRefresh; - - /// Show error tile on top - final bool showErrorTile; - - /// The builder that is used when the search messages are fetched - final Widget Function(List)? childBuilder; - - /// The builder used when the channel list is empty. - final WidgetBuilder? emptyBuilder; - - /// The builder that will be used in case of error - final ErrorBuilder? errorBuilder; - - /// The builder that will be used in case of loading - final WidgetBuilder? loadingBuilder; - - /// A [MessageSearchListController] allows reloading and pagination. - /// Use [MessageSearchListController.loadData] and - /// [MessageSearchListController.paginateData] respectively for reloading and - /// pagination. - final MessageSearchListController? messageSearchListController; - - @override - _MessageSearchListViewState createState() => _MessageSearchListViewState(); -} - -class _MessageSearchListViewState extends State { - late final _defaultController = MessageSearchListController(); - - MessageSearchListController get _messageSearchListController => - widget.messageSearchListController ?? _defaultController; - - @override - Widget build(BuildContext context) { - final messageSearchListCore = MessageSearchListCore( - filters: widget.filters, - sortOptions: widget.sortOptions, - messageQuery: widget.messageQuery, - limit: widget.limit, - messageFilters: widget.messageFilters, - messageSearchListController: _messageSearchListController, - emptyBuilder: widget.emptyBuilder ?? - (context) => LayoutBuilder( - builder: (context, viewportConstraints) => - SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Center( - child: Text(context.translations.emptyMessagesText), - ), - ), - ), - ), - errorBuilder: widget.errorBuilder ?? - (BuildContext context, dynamic error) { - if (error is Error) { - print(error.stackTrace); - } - return StreamInfoTile( - showMessage: widget.showErrorTile, - tileAnchor: Alignment.topCenter, - childAnchor: Alignment.topCenter, - message: context.translations.genericErrorText, - child: Container(), - ); - }, - loadingBuilder: widget.loadingBuilder ?? - (context) => LayoutBuilder( - builder: (context, viewportConstraints) => - SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: const Center( - child: CircularProgressIndicator(), - ), - ), - ), - ), - childBuilder: widget.childBuilder ?? _buildListView, - ); - - final backgroundColor = - StreamMessageSearchListViewTheme.of(context).backgroundColor; - - if (backgroundColor != null) { - return ColoredBox( - color: backgroundColor, - child: messageSearchListCore, - ); - } - - return messageSearchListCore; - } - - Widget _separatorBuilder(BuildContext context, int index) => Container( - height: 1, - color: StreamChatTheme.of(context).colorTheme.borders, - ); - - Widget _listItemBuilder( - BuildContext context, - GetMessageResponse getMessageResponse, - ) { - if (widget.itemBuilder != null) { - return widget.itemBuilder!(context, getMessageResponse); - } - return MessageSearchItem( - getMessageResponse: getMessageResponse, - onTap: () => widget.onItemTap!(getMessageResponse), - ); - } - - Widget _buildQueryProgressIndicator(context) { - final messageSearchBloc = MessageSearchBloc.of(context); - - return StreamBuilder( - stream: messageSearchBloc.queryMessagesLoading, - initialData: false, - builder: (context, snapshot) { - if (snapshot.hasError) { - return ColoredBox( - color: StreamChatTheme.of(context) - .colorTheme - .accentError - .withOpacity(0.2), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16), - child: Center( - child: Text(context.translations.loadingMessagesError), - ), - ), - ); - } - return Container( - height: 100, - padding: const EdgeInsets.all(32), - child: Center( - child: snapshot.data! - ? const CircularProgressIndicator() - : Container(), - ), - ); - }, - ); - } - - Widget _buildListView(List data) { - final items = data; - - Widget child = ListView.separated( - physics: const AlwaysScrollableScrollPhysics(), - itemCount: items.isNotEmpty ? items.length + 1 : items.length, - separatorBuilder: (_, index) { - if (widget.separatorBuilder != null) { - return widget.separatorBuilder!(context, index); - } - return _separatorBuilder(context, index); - }, - itemBuilder: (context, index) { - if (index < items.length) { - return _listItemBuilder(context, items[index]); - } - return _buildQueryProgressIndicator(context); - }, - ); - if (widget.pullToRefresh) { - child = RefreshIndicator( - onRefresh: () => _messageSearchListController.loadData!(), - child: child, - ); - } - - child = LazyLoadScrollView( - onEndOfPage: () => _messageSearchListController.paginateData!(), - child: child, - ); - - if (widget.showResultCount) { - final chatThemeData = StreamChatTheme.of(context); - child = Column( - children: [ - Container( - width: double.maxFinite, - decoration: BoxDecoration( - gradient: chatThemeData.colorTheme.bgGradient, - ), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 8, - ), - child: Text( - context.translations.resultCountText(items.length), - style: TextStyle( - color: chatThemeData.colorTheme.textLowEmphasis, - ), - ), - ), - ), - Expanded(child: child), - ], - ); - } - return child; - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart deleted file mode 100644 index 8164f08c..00000000 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ /dev/null @@ -1,1524 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_portal/flutter_portal.dart'; -import 'package:stream_chat_flutter/src/attachment/url_attachment.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/image_group.dart'; -import 'package:stream_chat_flutter/src/message_actions_modal.dart'; -import 'package:stream_chat_flutter/src/message_reactions_modal.dart'; -import 'package:stream_chat_flutter/src/quoted_message_widget.dart'; -import 'package:stream_chat_flutter/src/reaction_bubble.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// Widget builder for building attachments -typedef AttachmentBuilder = Widget Function( - BuildContext, - Message, - List, -); - -/// Callback for when quoted message is tapped -typedef OnQuotedMessageTap = void Function(String?); - -/// The display behaviour of a widget -enum DisplayWidget { - /// Hides the widget replacing its space with a spacer - hide, - - /// Hides the widget not replacing its space - gone, - - /// Shows the widget normally - show, -} - -/// {@macro message_widget} -@Deprecated("Use 'StreamMessageWidget' instead") -typedef MessageWidget = StreamMessageWidget; - -/// {@template message_widget} -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_widget.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_widget_paint.png) -/// -/// It shows a message with reactions, replies and user avatar. -/// -/// Usually you don't use this widget as it's the default message widget used by -/// [StreamMessageListView]. -/// -/// The widget components render the ui based on the first ancestor of type -/// [StreamChatTheme]. -/// Modify it to change the widget appearance. -/// {@endtemplate} -class StreamMessageWidget extends StatefulWidget { - /// Creates a new instance of the message widget. - StreamMessageWidget({ - super.key, - required this.message, - required this.messageTheme, - this.reverse = false, - this.translateUserAvatar = true, - this.shape, - this.attachmentShape, - this.borderSide, - this.attachmentBorderSide, - this.borderRadiusGeometry, - this.attachmentBorderRadiusGeometry, - this.onMentionTap, - this.onMessageTap, - this.showReactionPickerIndicator = false, - this.showUserAvatar = DisplayWidget.show, - this.showSendingIndicator = true, - this.showThreadReplyIndicator = false, - this.showInChannelIndicator = false, - this.onReplyTap, - this.onThreadTap, - this.showUsername = true, - this.showTimestamp = true, - this.showReactions = true, - this.showDeleteMessage = true, - this.showEditMessage = true, - this.showReplyMessage = true, - this.showThreadReplyMessage = true, - this.showResendMessage = true, - this.showCopyMessage = true, - this.showFlagButton = true, - this.showPinButton = true, - this.showPinHighlight = true, - this.onUserAvatarTap, - this.onLinkTap, - this.onMessageActions, - this.onShowMessage, - this.userAvatarBuilder, - this.editMessageInputBuilder, - this.textBuilder, - this.bottomRowBuilder, - this.deletedBottomRowBuilder, - this.onReturnAction, - this.customAttachmentBuilders, - this.padding, - this.textPadding = const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - this.attachmentPadding = EdgeInsets.zero, - this.onQuotedMessageTap, - this.customActions = const [], - this.onAttachmentTap, - this.usernameBuilder, - this.imageAttachmentThumbnailSize = const Size(400, 400), - this.imageAttachmentThumbnailResizeType = 'crop', - this.imageAttachmentThumbnailCropType = 'center', - }) : attachmentBuilders = { - 'image': (context, message, attachments) { - final border = RoundedRectangleBorder( - borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero, - ); - - final mediaQueryData = MediaQuery.of(context); - if (attachments.length > 1) { - return Padding( - padding: attachmentPadding, - child: wrapAttachmentWidget( - context, - Material( - color: messageTheme.messageBackgroundColor, - child: StreamImageGroup( - size: Size( - mediaQueryData.size.width * 0.8, - mediaQueryData.size.height * 0.3, - ), - images: attachments, - message: message, - messageTheme: messageTheme, - onShowMessage: onShowMessage, - onReturnAction: onReturnAction, - onAttachmentTap: onAttachmentTap, - imageThumbnailSize: imageAttachmentThumbnailSize, - imageThumbnailResizeType: - imageAttachmentThumbnailResizeType, - imageThumbnailCropType: imageAttachmentThumbnailCropType, - ), - ), - border, - reverse, - ), - ); - } - - return wrapAttachmentWidget( - context, - StreamImageAttachment( - attachment: attachments[0], - message: message, - messageTheme: messageTheme, - size: Size( - mediaQueryData.size.width * 0.8, - mediaQueryData.size.height * 0.3, - ), - onShowMessage: onShowMessage, - onReturnAction: onReturnAction, - onAttachmentTap: onAttachmentTap != null - ? () { - onAttachmentTap.call(message, attachments[0]); - } - : null, - imageThumbnailSize: imageAttachmentThumbnailSize, - imageThumbnailResizeType: imageAttachmentThumbnailResizeType, - imageThumbnailCropType: imageAttachmentThumbnailCropType, - ), - border, - reverse, - ); - }, - 'video': (context, message, attachments) { - final border = RoundedRectangleBorder( - borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero, - ); - - return wrapAttachmentWidget( - context, - Column( - children: attachments.map((attachment) { - final mediaQueryData = MediaQuery.of(context); - return StreamVideoAttachment( - attachment: attachment, - messageTheme: messageTheme, - size: Size( - mediaQueryData.size.width * 0.8, - mediaQueryData.size.height * 0.3, - ), - message: message, - onShowMessage: onShowMessage, - onReturnAction: onReturnAction, - onAttachmentTap: onAttachmentTap != null - ? () { - onAttachmentTap(message, attachment); - } - : null, - ); - }).toList(), - ), - border, - reverse, - ); - }, - 'giphy': (context, message, attachments) { - final border = RoundedRectangleBorder( - borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero, - ); - - return wrapAttachmentWidget( - context, - Column( - children: attachments.map((attachment) { - final mediaQueryData = MediaQuery.of(context); - return StreamGiphyAttachment( - attachment: attachment, - message: message, - size: Size( - mediaQueryData.size.width * 0.8, - mediaQueryData.size.height * 0.3, - ), - onShowMessage: onShowMessage, - onReturnAction: onReturnAction, - onAttachmentTap: onAttachmentTap != null - ? () { - onAttachmentTap(message, attachment); - } - : null, - ); - }).toList(), - ), - border, - reverse, - ); - }, - 'file': (context, message, attachments) { - final border = RoundedRectangleBorder( - side: attachmentBorderSide ?? - BorderSide( - color: StreamChatTheme.of(context).colorTheme.borders, - ), - borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero, - ); - - return Column( - children: attachments - .map((attachment) { - final mediaQueryData = MediaQuery.of(context); - return wrapAttachmentWidget( - context, - StreamFileAttachment( - message: message, - attachment: attachment, - size: Size( - mediaQueryData.size.width * 0.8, - mediaQueryData.size.height * 0.3, - ), - onAttachmentTap: onAttachmentTap != null - ? () { - onAttachmentTap(message, attachment); - } - : null, - ), - border, - reverse, - ); - }) - .insertBetween(SizedBox( - height: attachmentPadding.vertical / 2, - )) - .toList(), - ); - }, - }..addAll(customAttachmentBuilders ?? {}); - - /// Function called on mention tap - final void Function(User)? onMentionTap; - - /// The function called when tapping on threads - final void Function(Message)? onThreadTap; - - /// The function called when tapping on replies - final void Function(Message)? onReplyTap; - - /// Widget builder for edit message layout - final Widget Function(BuildContext, Message)? editMessageInputBuilder; - - /// Widget builder for building text - final Widget Function(BuildContext, Message)? textBuilder; - - /// Widget builder for building username - final Widget Function(BuildContext, Message)? usernameBuilder; - - /// Function called on long press - final void Function(BuildContext, Message)? onMessageActions; - - /// Widget builder for building a bottom row below the message - final Widget Function(BuildContext, Message)? bottomRowBuilder; - - /// Widget builder for building a bottom row below a deleted message - final Widget Function(BuildContext, Message)? deletedBottomRowBuilder; - - /// Widget builder for building user avatar - final Widget Function(BuildContext, User)? userAvatarBuilder; - - /// The message - final Message message; - - /// The message theme - final StreamMessageThemeData messageTheme; - - /// If true the widget will be mirrored - final bool reverse; - - /// The shape of the message text - final ShapeBorder? shape; - - /// The shape of an attachment - final ShapeBorder? attachmentShape; - - /// The borderside of the message text - final BorderSide? borderSide; - - /// The borderside of an attachment - final BorderSide? attachmentBorderSide; - - /// The border radius of the message text - final BorderRadiusGeometry? borderRadiusGeometry; - - /// The border radius of an attachment - final BorderRadiusGeometry? attachmentBorderRadiusGeometry; - - /// The padding of the widget - final EdgeInsetsGeometry? padding; - - /// The internal padding of the message text - final EdgeInsets textPadding; - - /// The internal padding of an attachment - final EdgeInsetsGeometry attachmentPadding; - - /// It controls the display behaviour of the user avatar - final DisplayWidget showUserAvatar; - - /// It controls the display behaviour of the sending indicator - final bool showSendingIndicator; - - /// If true the widget will show the reactions - final bool showReactions; - - /// If true the widget will show the thread reply indicator - final bool showThreadReplyIndicator; - - /// If true the widget will show the show in channel indicator - final bool showInChannelIndicator; - - /// The function called when tapping on UserAvatar - final void Function(User)? onUserAvatarTap; - - /// The function called when tapping on a link - final void Function(String)? onLinkTap; - - /// Used in [StreamMessageReactionsModal] and [StreamMessageActionsModal] - final bool showReactionPickerIndicator; - - /// Callback when show message is tapped - final ShowMessageCallback? onShowMessage; - - /// Handle return actions like reply message - final ValueChanged? onReturnAction; - - /// If true show the users username next to the timestamp of the message - final bool showUsername; - - /// Show message timestamp - final bool showTimestamp; - - /// Show reply action - final bool showReplyMessage; - - /// Show thread reply action - final bool showThreadReplyMessage; - - /// Show edit action - final bool showEditMessage; - - /// Show copy action - final bool showCopyMessage; - - /// Show delete action - final bool showDeleteMessage; - - /// Show resend action - final bool showResendMessage; - - /// Show flag action - final bool showFlagButton; - - /// Show flag action - final bool showPinButton; - - /// Display Pin Highlight - final bool showPinHighlight; - - /// Builder for respective attachment types - final Map attachmentBuilders; - - /// Builder for respective attachment types (user facing builder) - final Map? customAttachmentBuilders; - - /// Center user avatar with bottom of the message - final bool translateUserAvatar; - - /// Function called when quotedMessage is tapped - final OnQuotedMessageTap? onQuotedMessageTap; - - /// Function called when message is tapped - final void Function(Message)? onMessageTap; - - /// List of custom actions shown on message long tap - final List customActions; - - /// Customize onTap on attachment - final void Function(Message message, Attachment attachment)? onAttachmentTap; - - /// Size of the image attachment thumbnail. - final Size imageAttachmentThumbnailSize; - - /// Resize type of the image attachment thumbnail. - /// - /// Defaults to [crop] - final String /*clip|crop|scale|fill*/ imageAttachmentThumbnailResizeType; - - /// Crop type of the image attachment thumbnail. - /// - /// Defaults to [center] - final String /*center|top|bottom|left|right*/ - imageAttachmentThumbnailCropType; - - /// Creates a copy of [StreamMessageWidget] with - /// specified attributes overridden. - StreamMessageWidget copyWith({ - Key? key, - void Function(User)? onMentionTap, - void Function(Message)? onThreadTap, - void Function(Message)? onReplyTap, - Widget Function(BuildContext, Message)? editMessageInputBuilder, - Widget Function(BuildContext, Message)? textBuilder, - Widget Function(BuildContext, Message)? usernameBuilder, - Widget Function(BuildContext, Message)? bottomRowBuilder, - Widget Function(BuildContext, Message)? deletedBottomRowBuilder, - void Function(BuildContext, Message)? onMessageActions, - Message? message, - StreamMessageThemeData? messageTheme, - bool? reverse, - ShapeBorder? shape, - ShapeBorder? attachmentShape, - BorderSide? borderSide, - BorderSide? attachmentBorderSide, - BorderRadiusGeometry? borderRadiusGeometry, - BorderRadiusGeometry? attachmentBorderRadiusGeometry, - EdgeInsetsGeometry? padding, - EdgeInsets? textPadding, - EdgeInsetsGeometry? attachmentPadding, - DisplayWidget? showUserAvatar, - bool? showSendingIndicator, - bool? showReactions, - bool? allRead, - bool? showThreadReplyIndicator, - bool? showInChannelIndicator, - void Function(User)? onUserAvatarTap, - void Function(String)? onLinkTap, - bool? showReactionPickerIndicator, - List? readList, - ShowMessageCallback? onShowMessage, - ValueChanged? onReturnAction, - bool? showUsername, - bool? showTimestamp, - bool? showReplyMessage, - bool? showThreadReplyMessage, - bool? showEditMessage, - bool? showCopyMessage, - bool? showDeleteMessage, - bool? showResendMessage, - bool? showFlagButton, - bool? showPinButton, - bool? showPinHighlight, - Map? customAttachmentBuilders, - bool? translateUserAvatar, - OnQuotedMessageTap? onQuotedMessageTap, - void Function(Message)? onMessageTap, - List? customActions, - void Function(Message message, Attachment attachment)? onAttachmentTap, - Widget Function(BuildContext, User)? userAvatarBuilder, - Size? imageAttachmentThumbnailSize, - String? imageAttachmentThumbnailResizeType, - String? imageAttachmentThumbnailCropType, - }) => - StreamMessageWidget( - key: key ?? this.key, - onMentionTap: onMentionTap ?? this.onMentionTap, - onThreadTap: onThreadTap ?? this.onThreadTap, - onReplyTap: onReplyTap ?? this.onReplyTap, - editMessageInputBuilder: - editMessageInputBuilder ?? this.editMessageInputBuilder, - textBuilder: textBuilder ?? this.textBuilder, - usernameBuilder: usernameBuilder ?? this.usernameBuilder, - bottomRowBuilder: bottomRowBuilder ?? this.bottomRowBuilder, - deletedBottomRowBuilder: - deletedBottomRowBuilder ?? this.deletedBottomRowBuilder, - onMessageActions: onMessageActions ?? this.onMessageActions, - message: message ?? this.message, - messageTheme: messageTheme ?? this.messageTheme, - reverse: reverse ?? this.reverse, - shape: shape ?? this.shape, - attachmentShape: attachmentShape ?? this.attachmentShape, - borderSide: borderSide ?? this.borderSide, - attachmentBorderSide: attachmentBorderSide ?? this.attachmentBorderSide, - borderRadiusGeometry: borderRadiusGeometry ?? this.borderRadiusGeometry, - attachmentBorderRadiusGeometry: attachmentBorderRadiusGeometry ?? - this.attachmentBorderRadiusGeometry, - padding: padding ?? this.padding, - textPadding: textPadding ?? this.textPadding, - attachmentPadding: attachmentPadding ?? this.attachmentPadding, - showUserAvatar: showUserAvatar ?? this.showUserAvatar, - showSendingIndicator: showSendingIndicator ?? this.showSendingIndicator, - showReactions: showReactions ?? this.showReactions, - showThreadReplyIndicator: - showThreadReplyIndicator ?? this.showThreadReplyIndicator, - showInChannelIndicator: - showInChannelIndicator ?? this.showInChannelIndicator, - onUserAvatarTap: onUserAvatarTap ?? this.onUserAvatarTap, - onLinkTap: onLinkTap ?? this.onLinkTap, - showReactionPickerIndicator: - showReactionPickerIndicator ?? this.showReactionPickerIndicator, - onShowMessage: onShowMessage ?? this.onShowMessage, - onReturnAction: onReturnAction ?? this.onReturnAction, - showUsername: showUsername ?? this.showUsername, - showTimestamp: showTimestamp ?? this.showTimestamp, - showReplyMessage: showReplyMessage ?? this.showReplyMessage, - showThreadReplyMessage: - showThreadReplyMessage ?? this.showThreadReplyMessage, - showEditMessage: showEditMessage ?? this.showEditMessage, - showCopyMessage: showCopyMessage ?? this.showCopyMessage, - showDeleteMessage: showDeleteMessage ?? this.showDeleteMessage, - showResendMessage: showResendMessage ?? this.showResendMessage, - showFlagButton: showFlagButton ?? this.showFlagButton, - showPinButton: showPinButton ?? this.showPinButton, - showPinHighlight: showPinHighlight ?? this.showPinHighlight, - customAttachmentBuilders: - customAttachmentBuilders ?? this.customAttachmentBuilders, - translateUserAvatar: translateUserAvatar ?? this.translateUserAvatar, - onQuotedMessageTap: onQuotedMessageTap ?? this.onQuotedMessageTap, - onMessageTap: onMessageTap ?? this.onMessageTap, - customActions: customActions ?? this.customActions, - onAttachmentTap: onAttachmentTap ?? this.onAttachmentTap, - userAvatarBuilder: userAvatarBuilder ?? this.userAvatarBuilder, - imageAttachmentThumbnailSize: - imageAttachmentThumbnailSize ?? this.imageAttachmentThumbnailSize, - imageAttachmentThumbnailResizeType: - imageAttachmentThumbnailResizeType ?? - this.imageAttachmentThumbnailResizeType, - imageAttachmentThumbnailCropType: imageAttachmentThumbnailCropType ?? - this.imageAttachmentThumbnailCropType, - ); - - @override - _StreamMessageWidgetState createState() => _StreamMessageWidgetState(); -} - -class _StreamMessageWidgetState extends State - with AutomaticKeepAliveClientMixin { - bool get showThreadReplyIndicator => widget.showThreadReplyIndicator; - - bool get showSendingIndicator => widget.showSendingIndicator; - - bool get isDeleted => widget.message.isDeleted; - - bool get showUsername => widget.showUsername; - - bool get showTimeStamp => widget.showTimestamp; - - bool get showInChannel => widget.showInChannelIndicator; - - bool get hasQuotedMessage => widget.message.quotedMessage != null; - - bool get isSendFailed => widget.message.status == MessageSendingStatus.failed; - - bool get isUpdateFailed => - widget.message.status == MessageSendingStatus.failed_update; - - bool get isDeleteFailed => - widget.message.status == MessageSendingStatus.failed_delete; - - bool get isFailedState => isSendFailed || isUpdateFailed || isDeleteFailed; - - bool get isGiphy => - widget.message.attachments.any((element) => element.type == 'giphy'); - - bool get isOnlyEmoji => widget.message.text?.isOnlyEmoji == true; - - bool get hasNonUrlAttachments => widget.message.attachments - .where((it) => it.ogScrapeUrl == null || it.type == 'giphy') - .isNotEmpty; - - bool get hasUrlAttachments => widget.message.attachments - .any((it) => it.ogScrapeUrl != null && it.type != 'giphy'); - - bool get showBottomRow => - showThreadReplyIndicator || - showUsername || - showTimeStamp || - showInChannel || - showSendingIndicator || - isDeleted; - - @override - bool get wantKeepAlive => widget.message.attachments.isNotEmpty; - - late StreamChatThemeData _streamChatTheme; - late StreamChatState _streamChat; - - @override - Widget build(BuildContext context) { - super.build(context); - final avatarWidth = - widget.messageTheme.avatarTheme?.constraints.maxWidth ?? 40; - final bottomRowPadding = - widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5; - - final showReactions = _shouldShowReactions; - - final onMessageTap = widget.onMessageTap; - - return Material( - type: MaterialType.transparency, - child: AnimatedContainer( - duration: const Duration(seconds: 1), - color: widget.message.pinned && widget.showPinHighlight - ? _streamChatTheme.colorTheme.highlight - : _streamChatTheme.colorTheme.barsBg.withOpacity(0), - child: Portal( - child: InkWell( - onTap: onMessageTap == null - ? null - : () => onMessageTap(widget.message), - onLongPress: widget.message.isDeleted && !isFailedState - ? null - : () => onLongPress(context), - child: Padding( - padding: widget.padding ?? const EdgeInsets.all(8), - child: FractionallySizedBox( - alignment: widget.reverse - ? Alignment.centerRight - : Alignment.centerLeft, - widthFactor: 0.78, - child: Column( - crossAxisAlignment: widget.reverse - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Stack( - clipBehavior: Clip.none, - alignment: widget.reverse - ? AlignmentDirectional.bottomEnd - : AlignmentDirectional.bottomStart, - children: [ - Padding( - padding: EdgeInsets.only( - bottom: - isPinned && widget.showPinHighlight ? 8.0 : 0.0, - ), - child: Column( - crossAxisAlignment: widget.reverse - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.message.pinned && - widget.message.pinnedBy != null && - widget.showPinHighlight) - _buildPinnedMessage(widget.message), - Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - if (!widget.reverse && - widget.showUserAvatar == - DisplayWidget.show && - widget.message.user != null) ...[ - _buildUserAvatar(), - const SizedBox(width: 4), - ], - if (widget.showUserAvatar == - DisplayWidget.hide) - SizedBox(width: avatarWidth + 4), - Flexible( - child: PortalTarget( - visible: showReactions, - portalFollower: showReactions - ? Container( - transform: - Matrix4.translationValues( - widget.reverse ? 12 : -12, - 0, - 0, - ), - constraints: const BoxConstraints( - maxWidth: 22 * 6.0, - ), - child: _buildReactionIndicator( - context, - ), - ) - : null, - anchor: Aligned( - follower: Alignment( - widget.reverse ? 1 : -1, - -1, - ), - target: Alignment( - widget.reverse ? -1 : 1, - -1, - ), - ), - child: Stack( - clipBehavior: Clip.none, - children: [ - Padding( - padding: widget.showReactions - ? EdgeInsets.only( - top: widget - .message - .reactionCounts - ?.isNotEmpty == - true - ? 18 - : 0, - ) - : EdgeInsets.zero, - child: (widget.message.isDeleted && - !isFailedState) - ? Container( - margin: - EdgeInsets.symmetric( - horizontal: - // ignore: lines_longer_than_80_chars - widget.showUserAvatar == - // ignore: lines_longer_than_80_chars - DisplayWidget.gone - ? 0 - : 4.0, - ), - child: StreamDeletedMessage( - // ignore: lines_longer_than_80_chars - borderRadiusGeometry: widget - .borderRadiusGeometry, - borderSide: - widget.borderSide, - shape: widget.shape, - messageTheme: - widget.messageTheme, - ), - ) - : Card( - elevation: 0, - margin: - EdgeInsets.symmetric( - horizontal: (isFailedState - ? 15.0 - : 0.0) + - // ignore: lines_longer_than_80_chars - (widget.showUserAvatar == - DisplayWidget - .gone - ? 0 - : 4.0), - ), - shape: widget.shape ?? - RoundedRectangleBorder( - side: widget - .borderSide ?? - BorderSide( - color: widget - // ignore: lines_longer_than_80_chars - .messageTheme - // ignore: lines_longer_than_80_chars - .messageBorderColor ?? - Colors.grey, - ), - borderRadius: widget - // ignore: lines_longer_than_80_chars - .borderRadiusGeometry ?? - BorderRadius.zero, - ), - color: _backgroundColor, - child: Column( - crossAxisAlignment: - CrossAxisAlignment - .end, - mainAxisSize: - MainAxisSize.min, - children: [ - if (hasQuotedMessage) - _buildQuotedMessage(), - // ignore: lines_longer_than_80_chars - if (hasNonUrlAttachments) - _parseAttachments(), - if (!isGiphy) - _buildTextBubble(), - ], - ), - ), - ), - if (widget - .showReactionPickerIndicator) - Positioned( - right: widget.reverse ? null : 4, - left: widget.reverse ? 4 : null, - top: -8, - child: CustomPaint( - painter: ReactionBubblePainter( - _streamChatTheme - .colorTheme.barsBg, - Colors.transparent, - Colors.transparent, - tailCirclesSpace: 1, - ), - ), - ), - ], - ), - ), - ), - if (widget.reverse && - widget.showUserAvatar == - DisplayWidget.show && - widget.message.user != null) ...[ - _buildUserAvatar(), - const SizedBox(width: 4), - ], - ], - ), - if (showBottomRow) - SizedBox( - height: context.textScaleFactor * 18.0, - ), - ], - ), - ), - if (showBottomRow) - Padding( - padding: EdgeInsets.only( - left: !widget.reverse ? bottomRowPadding : 0, - right: widget.reverse ? bottomRowPadding : 0, - bottom: isPinned && widget.showPinHighlight - ? 6.0 - : 0.0, - ), - child: widget.bottomRowBuilder?.call( - context, - widget.message, - ) ?? - _bottomRow, - ), - if (isFailedState) - Positioned( - right: widget.reverse ? 0 : null, - left: widget.reverse ? null : 0, - bottom: showBottomRow ? 18 : -2, - child: StreamSvgIcon.error(size: 20), - ), - ], - ), - ], - ), - ), - ), - ), - ), - ), - ); - } - - @override - void didChangeDependencies() { - _streamChatTheme = StreamChatTheme.of(context); - _streamChat = StreamChat.of(context); - super.didChangeDependencies(); - } - - Widget _buildQuotedMessage() { - final isMyMessage = widget.message.user?.id == _streamChat.currentUser?.id; - final onTap = widget.message.quotedMessage?.isDeleted != true && - widget.onQuotedMessageTap != null - ? () => widget.onQuotedMessageTap!(widget.message.quotedMessageId) - : null; - final chatThemeData = _streamChatTheme; - return StreamQuotedMessageWidget( - onTap: onTap, - message: widget.message.quotedMessage!, - messageTheme: isMyMessage - ? chatThemeData.otherMessageTheme - : chatThemeData.ownMessageTheme, - reverse: widget.reverse, - padding: EdgeInsets.only( - right: 8, - left: 8, - top: 8, - bottom: hasNonUrlAttachments ? 8 : 0, - ), - ); - } - - Widget get _bottomRow { - if (isDeleted) { - return widget.deletedBottomRowBuilder?.call( - context, - widget.message, - ) ?? - const Offstage(); - } - - final children = []; - - final threadParticipants = widget.message.threadParticipants?.take(2); - final showThreadParticipants = threadParticipants?.isNotEmpty == true; - final replyCount = widget.message.replyCount; - - var msg = context.translations.threadReplyLabel; - if (showThreadReplyIndicator && replyCount! > 1) { - msg = context.translations.threadReplyCountText(replyCount); - } - - // ignore: prefer_function_declarations_over_variables - final onThreadTap = () async { - try { - var message = widget.message; - if (showInChannel) { - final channel = StreamChannel.of(context); - message = await channel.getMessage(widget.message.parentId!); - } - return widget.onThreadTap!(message); - } catch (e, stk) { - print(e); - print(stk); - // ignore: avoid_returning_null_for_void - return null; - } - }; - - const usernameKey = Key('username'); - - children.addAll([ - if (showUsername) WidgetSpan(child: _buildUsername(usernameKey)), - if (showTimeStamp) - WidgetSpan( - child: Text( - Jiffy(widget.message.createdAt.toLocal()).jm, - style: widget.messageTheme.createdAtStyle, - ), - ), - if (showSendingIndicator) - WidgetSpan( - child: _buildSendingIndicator(), - ), - ]); - - final showThreadTail = !(hasUrlAttachments || isGiphy || isOnlyEmoji) && - (showThreadReplyIndicator || showInChannel); - - final threadIndicatorWidgets = [ - if (showThreadTail) - WidgetSpan( - child: Container( - margin: EdgeInsets.only( - bottom: context.textScaleFactor * - ((widget.messageTheme.repliesStyle?.fontSize ?? 1) / 2), - ), - child: CustomPaint( - size: const Size(16, 32) * context.textScaleFactor, - painter: _ThreadReplyPainter( - context: context, - color: widget.messageTheme.messageBorderColor, - reverse: widget.reverse, - ), - ), - ), - ), - if (showInChannel || showThreadReplyIndicator) ...[ - if (showThreadParticipants) - WidgetSpan( - child: SizedBox.fromSize( - size: Size((threadParticipants!.length * 8.0) + 8, 16), - child: _buildThreadParticipantsIndicator(threadParticipants), - ), - ), - WidgetSpan( - child: InkWell( - onTap: widget.onThreadTap != null ? onThreadTap : null, - child: Text(msg, style: widget.messageTheme.repliesStyle), - ), - ), - ], - ]; - - if (widget.reverse) { - children.addAll(threadIndicatorWidgets.reversed); - } else { - children.insertAll(0, threadIndicatorWidgets); - } - - return Text.rich( - TextSpan( - children: [ - ...children, - ].insertBetween(const WidgetSpan(child: SizedBox(width: 8))), - ), - maxLines: 1, - textAlign: widget.reverse ? TextAlign.right : TextAlign.left, - ); - } - - Widget _buildUsername(Key usernameKey) { - if (widget.usernameBuilder != null) { - return widget.usernameBuilder!(context, widget.message); - } - return Text( - widget.message.user?.name ?? '', - maxLines: 1, - key: usernameKey, - style: widget.messageTheme.messageAuthorStyle, - overflow: TextOverflow.ellipsis, - ); - } - - Widget _buildUrlAttachment() { - final urlAttachment = widget.message.attachments - .firstWhere((element) => element.ogScrapeUrl != null); - - final host = Uri.parse(urlAttachment.ogScrapeUrl!).withScheme.host; - final splitList = host.split('.'); - final hostName = splitList.length == 3 ? splitList[1] : splitList[0]; - final hostDisplayName = urlAttachment.authorName?.capitalize() ?? - getWebsiteName(hostName.toLowerCase()) ?? - hostName.capitalize(); - - return StreamUrlAttachment( - urlAttachment: urlAttachment, - hostDisplayName: hostDisplayName, - textPadding: widget.textPadding, - messageTheme: widget.messageTheme, - onLinkTap: widget.onLinkTap, - ); - } - - Widget _buildThreadParticipantsIndicator(Iterable threadParticipants) => - _ThreadParticipants( - streamChatTheme: _streamChatTheme, - threadParticipants: threadParticipants, - ); - - Widget _buildReactionIndicator( - BuildContext context, - ) { - final ownId = _streamChat.currentUser!.id; - final reactionsMap = {}; - widget.message.latestReactions?.forEach((element) { - if (!reactionsMap.containsKey(element.type) || - element.user!.id == ownId) { - reactionsMap[element.type] = element; - } - }); - final reactionsList = reactionsMap.values.toList() - ..sort((a, b) => a.user!.id == ownId ? 1 : -1); - - return AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: _shouldShowReactions - ? GestureDetector( - onTap: () => _showMessageReactionsModalBottomSheet(context), - child: StreamReactionBubble( - key: ValueKey('${widget.message.id}.reactions'), - reverse: widget.reverse, - flipTail: widget.reverse, - backgroundColor: widget.messageTheme.reactionsBackgroundColor ?? - Colors.transparent, - borderColor: widget.messageTheme.reactionsBorderColor ?? - Colors.transparent, - maskColor: widget.messageTheme.reactionsMaskColor ?? - Colors.transparent, - reactions: reactionsList, - ), - ) - : const SizedBox(), - ); - } - - bool get _shouldShowReactions => - widget.showReactions && - (widget.message.reactionCounts?.isNotEmpty == true) && - !widget.message.isDeleted; - - void _showMessageActionModalBottomSheet(BuildContext context) { - final channel = StreamChannel.of(context).channel; - - showDialog( - useRootNavigator: false, - context: context, - barrierColor: _streamChatTheme.colorTheme.overlay, - builder: (context) => StreamChannel( - channel: channel, - child: StreamMessageActionsModal( - messageWidget: widget.copyWith( - key: const Key('MessageWidget'), - message: widget.message.copyWith( - text: (widget.message.text?.length ?? 0) > 200 - ? '${widget.message.text!.substring(0, 200)}...' - : widget.message.text, - ), - showReactions: false, - showUsername: false, - showTimestamp: false, - translateUserAvatar: false, - showSendingIndicator: false, - padding: EdgeInsets.zero, - showReactionPickerIndicator: widget.showReactions && - (widget.message.status == MessageSendingStatus.sent) && - channel.ownCapabilities.contains(PermissionType.sendReaction), - showPinHighlight: false, - showUserAvatar: - widget.message.user!.id == channel.client.state.currentUser!.id - ? DisplayWidget.gone - : DisplayWidget.show, - ), - onCopyTap: (message) => - Clipboard.setData(ClipboardData(text: message.text)), - messageTheme: widget.messageTheme, - reverse: widget.reverse, - message: widget.message, - editMessageInputBuilder: widget.editMessageInputBuilder, - onReplyTap: widget.onReplyTap, - onThreadReplyTap: widget.onThreadTap, - showResendMessage: - widget.showResendMessage && (isSendFailed || isUpdateFailed), - showCopyMessage: widget.showCopyMessage && - !isFailedState && - widget.message.text?.trim().isNotEmpty == true, - showReplyMessage: widget.showReplyMessage && - !isFailedState && - widget.onReplyTap != null, - showThreadReplyMessage: widget.showThreadReplyMessage && - !isFailedState && - widget.onThreadTap != null, - showFlagButton: widget.showFlagButton, - customActions: widget.customActions, - showDeleteMessage: widget.showDeleteMessage || isDeleteFailed, - showEditMessage: widget.showEditMessage && - !isDeleteFailed && - !widget.message.attachments - .any((element) => element.type == 'giphy'), - showPinButton: widget.showPinButton, - showReactions: widget.showReactions, - ), - ), - ); - } - - void _showMessageReactionsModalBottomSheet(BuildContext context) { - final channel = StreamChannel.of(context).channel; - showDialog( - useRootNavigator: false, - context: context, - barrierColor: _streamChatTheme.colorTheme.overlay, - builder: (context) => StreamChannel( - channel: channel, - child: StreamMessageReactionsModal( - messageWidget: widget.copyWith( - key: const Key('MessageWidget'), - message: widget.message.copyWith( - text: (widget.message.text?.length ?? 0) > 200 - ? '${widget.message.text!.substring(0, 200)}...' - : widget.message.text, - ), - showReactions: false, - showUsername: false, - showTimestamp: false, - translateUserAvatar: false, - showSendingIndicator: false, - padding: EdgeInsets.zero, - showReactionPickerIndicator: widget.showReactions && - (widget.message.status == MessageSendingStatus.sent) && - channel.ownCapabilities.contains(PermissionType.sendReaction), - showPinHighlight: false, - showUserAvatar: - widget.message.user!.id == channel.client.state.currentUser!.id - ? DisplayWidget.gone - : DisplayWidget.show, - ), - onUserAvatarTap: widget.onUserAvatarTap, - messageTheme: widget.messageTheme, - reverse: widget.reverse, - message: widget.message, - showReactions: widget.showReactions && - channel.ownCapabilities.contains(PermissionType.sendReaction), - ), - ), - ); - } - - Widget _parseAttachments() { - final attachmentGroups = >{}; - - widget.message.attachments - .where((element) => - (element.ogScrapeUrl == null && element.type != null) || - element.type == 'giphy') - .forEach((e) { - if (attachmentGroups[e.type] == null) { - attachmentGroups[e.type!] = []; - } - - attachmentGroups[e.type]?.add(e); - }); - - final attachmentList = []; - - attachmentGroups.forEach((type, attachments) { - final attachmentBuilder = widget.attachmentBuilders[type]; - - if (attachmentBuilder == null) return; - final attachmentWidget = attachmentBuilder( - context, - widget.message, - attachments, - ); - attachmentList.add(attachmentWidget); - }); - - return Padding( - padding: widget.attachmentPadding, - child: Column( - mainAxisSize: MainAxisSize.min, - children: attachmentList.insertBetween(SizedBox( - height: widget.attachmentPadding.vertical / 2, - )), - ), - ); - } - - void onLongPress(BuildContext context) { - if (widget.message.isEphemeral || - widget.message.status == MessageSendingStatus.sending) { - return; - } - - if (widget.onMessageActions != null) { - widget.onMessageActions!(context, widget.message); - } else { - _showMessageActionModalBottomSheet(context); - } - return; - } - - Widget _buildSendingIndicator() { - final style = widget.messageTheme.createdAtStyle; - final message = widget.message; - final memberCount = StreamChannel.of(context).channel.memberCount ?? 0; - - if (hasNonUrlAttachments && - (message.status == MessageSendingStatus.sending || - message.status == MessageSendingStatus.updating)) { - final totalAttachments = message.attachments.length; - final uploadRemaining = - message.attachments.where((it) => !it.uploadState.isSuccess).length; - if (uploadRemaining == 0) { - return StreamSvgIcon.check( - size: style!.fontSize, - color: IconTheme.of(context).color!.withOpacity(0.5), - ); - } - return Text( - context.translations.attachmentsUploadProgressText( - remaining: uploadRemaining, - total: totalAttachments, - ), - style: style, - ); - } - - final channel = StreamChannel.of(context).channel; - - if (!channel.ownCapabilities.contains(PermissionType.readEvents)) { - return StreamSendingIndicator( - message: message, - size: style!.fontSize, - ); - } - - return BetterStreamBuilder>( - stream: channel.state?.readStream, - initialData: channel.state?.read, - builder: (context, data) { - final readList = data.where((it) => - it.user.id != _streamChat.currentUser?.id && - (it.lastRead.isAfter(message.createdAt) || - it.lastRead.isAtSameMomentAs(message.createdAt))); - final isMessageRead = readList.length >= (channel.memberCount ?? 0) - 1; - Widget child = StreamSendingIndicator( - message: message, - isMessageRead: isMessageRead, - size: style!.fontSize, - ); - if (isMessageRead) { - child = Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (memberCount > 2) - Text( - readList.length.toString(), - style: style.copyWith( - color: _streamChatTheme.colorTheme.accentPrimary, - ), - ), - const SizedBox(width: 2), - child, - ], - ); - } - return child; - }, - ); - } - - Widget _buildUserAvatar() => Transform.translate( - offset: Offset( - 0, - widget.translateUserAvatar - ? (widget.messageTheme.avatarTheme?.constraints.maxHeight ?? 40) / - 2 - : 0, - ), - child: widget.userAvatarBuilder?.call(context, widget.message.user!) ?? - StreamUserAvatar( - user: widget.message.user!, - onTap: widget.onUserAvatarTap, - constraints: widget.messageTheme.avatarTheme!.constraints, - borderRadius: widget.messageTheme.avatarTheme!.borderRadius, - showOnlineStatus: false, - ), - ); - - Widget _buildTextBubble() { - if (widget.message.text?.trim().isEmpty ?? false) return const Offstage(); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: isOnlyEmoji ? EdgeInsets.zero : widget.textPadding, - child: widget.textBuilder != null - ? widget.textBuilder!(context, widget.message) - : StreamMessageText( - onLinkTap: widget.onLinkTap, - message: widget.message, - onMentionTap: widget.onMentionTap, - messageTheme: isOnlyEmoji - ? widget.messageTheme.copyWith( - messageTextStyle: - widget.messageTheme.messageTextStyle!.copyWith( - fontSize: 42, - ), - ) - : widget.messageTheme, - ), - ), - if (hasUrlAttachments && !hasQuotedMessage) _buildUrlAttachment(), - ], - ); - } - - Widget _buildPinnedMessage(Message message) { - final pinnedBy = message.pinnedBy!; - final currentUser = _streamChat.currentUser!; - - return Padding( - padding: const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 8), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - StreamSvgIcon.pin(size: 16), - const SizedBox(width: 4), - Text( - context.translations.pinnedByUserText( - pinnedBy: pinnedBy, - currentUser: currentUser, - ), - style: TextStyle( - color: _streamChatTheme.colorTheme.textLowEmphasis, - fontSize: 13, - fontWeight: FontWeight.w400, - ), - ), - ], - ), - ); - } - - bool get isPinned => widget.message.pinned; - - Color? get _backgroundColor { - if (hasQuotedMessage) { - return widget.messageTheme.messageBackgroundColor; - } - - if (hasUrlAttachments) { - return widget.messageTheme.linkBackgroundColor; - } - - if (isOnlyEmoji) { - return Colors.transparent; - } - - if (isGiphy) { - return Colors.transparent; - } - - return widget.messageTheme.messageBackgroundColor; - } - - void retryMessage(BuildContext context) { - final channel = StreamChannel.of(context).channel; - if (widget.message.status == MessageSendingStatus.failed) { - channel.sendMessage(widget.message); - return; - } - if (widget.message.status == MessageSendingStatus.failed_update) { - channel.updateMessage(widget.message); - return; - } - - if (widget.message.status == MessageSendingStatus.failed_delete) { - channel.deleteMessage(widget.message); - return; - } - } -} - -class _ThreadParticipants extends StatelessWidget { - const _ThreadParticipants({ - required StreamChatThemeData streamChatTheme, - required this.threadParticipants, - }) : _streamChatTheme = streamChatTheme; - - final StreamChatThemeData _streamChatTheme; - final Iterable threadParticipants; - - @override - Widget build(BuildContext context) { - var padding = 0.0; - return Stack( - children: threadParticipants.map((user) { - padding += 8.0; - return Positioned( - right: padding - 8, - bottom: 0, - top: 0, - child: Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: _streamChatTheme.colorTheme.barsBg, - ), - padding: const EdgeInsets.all(1), - child: StreamUserAvatar( - user: user, - constraints: BoxConstraints.tight(const Size.fromRadius(7)), - showOnlineStatus: false, - ), - ), - ); - }).toList(), - ); - } -} - -class _ThreadReplyPainter extends CustomPainter { - const _ThreadReplyPainter({ - this.context, - required this.color, - this.reverse = false, - }); - - final Color? color; - final BuildContext? context; - final bool reverse; - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = color ?? StreamChatTheme.of(context!).colorTheme.disabled - ..style = PaintingStyle.stroke - ..strokeWidth = 1 - ..strokeCap = StrokeCap.round; - - final path = Path() - ..moveTo(reverse ? size.width : 0, 0) - ..quadraticBezierTo( - reverse ? size.width : 0, - size.height * 0.38, - reverse ? size.width : 0, - size.height * 0.5, - ) - ..quadraticBezierTo( - reverse ? size.width : 0, - size.height, - reverse ? 0 : size.width, - size.height, - ); - canvas.drawPath(path, paint); - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; -} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/bottom_row.dart b/packages/stream_chat_flutter/lib/src/message_widget/bottom_row.dart new file mode 100644 index 00000000..1a326af4 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/bottom_row.dart @@ -0,0 +1,221 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/message_widget/sending_indicator_wrapper.dart'; +import 'package:stream_chat_flutter/src/message_widget/thread_painter.dart'; +import 'package:stream_chat_flutter/src/message_widget/thread_participants.dart'; +import 'package:stream_chat_flutter/src/message_widget/username.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template bottomRow} +/// The bottom row of a [StreamMessageWidget]. +/// +/// Used in [MessageWidgetContent]. Should not be used elsewhere. +/// {@endtemplate} +class BottomRow extends StatelessWidget { + /// {@macro bottomRow} + const BottomRow({ + super.key, + required this.isDeleted, + required this.message, + required this.showThreadReplyIndicator, + required this.showInChannel, + required this.showTimeStamp, + required this.showUsername, + required this.reverse, + required this.showSendingIndicator, + required this.hasUrlAttachments, + required this.isGiphy, + required this.isOnlyEmoji, + required this.messageTheme, + required this.streamChatTheme, + required this.hasNonUrlAttachments, + required this.streamChat, + this.deletedBottomRowBuilder, + this.onThreadTap, + this.usernameBuilder, + }); + + /// {@macro messageIsDeleted} + final bool isDeleted; + + /// {@macro deletedBottomRowBuilder} + final Widget Function(BuildContext, Message)? deletedBottomRowBuilder; + + /// {@macro message} + final Message message; + + /// {@macro showThreadReplyIndicator} + final bool showThreadReplyIndicator; + + /// {@macro showInChannelIndicator} + final bool showInChannel; + + /// {@macro showTimestamp} + final bool showTimeStamp; + + /// {@macro showUsername} + final bool showUsername; + + /// {@macro reverse} + final bool reverse; + + /// {@macro showSendingIndicator} + final bool showSendingIndicator; + + /// {@macro hasUrlAttachments} + final bool hasUrlAttachments; + + /// {@macro isGiphy} + final bool isGiphy; + + /// {@macro isOnlyEmoji} + final bool isOnlyEmoji; + + /// {@macro hasNonUrlAttachments} + final bool hasNonUrlAttachments; + + /// {@macro messageTheme} + final StreamMessageThemeData messageTheme; + + /// {@macro onThreadTap} + final void Function(Message)? onThreadTap; + + /// {@macro streamChatThemeData} + final StreamChatThemeData streamChatTheme; + + /// {@macro streamChat} + final StreamChatState streamChat; + + /// {@macro usernameBuilder} + final Widget Function(BuildContext, Message)? usernameBuilder; + + @override + Widget build(BuildContext context) { + if (isDeleted) { + return deletedBottomRowBuilder?.call( + context, + message, + ) ?? + const Offstage(); + } + + final children = []; + + final threadParticipants = message.threadParticipants?.take(2); + final showThreadParticipants = threadParticipants?.isNotEmpty == true; + final replyCount = message.replyCount; + + var msg = context.translations.threadReplyLabel; + if (showThreadReplyIndicator && replyCount! > 1) { + msg = context.translations.threadReplyCountText(replyCount); + } + + // ignore: prefer_function_declarations_over_variables + final _onThreadTap = () async { + try { + var message = this.message; + if (showInChannel) { + final channel = StreamChannel.of(context); + message = await channel.getMessage(message.parentId!); + } + return onThreadTap!(message); + } catch (e, stk) { + print(e); + print(stk); + // ignore: avoid_returning_null_for_void + return null; + } + }; + + const usernameKey = Key('username'); + + children.addAll([ + if (showUsername) + WidgetSpan( + child: usernameBuilder?.call(context, message) ?? + Username( + key: usernameKey, + message: message, + messageTheme: messageTheme, + ), + ), + if (showTimeStamp) + WidgetSpan( + child: Text( + Jiffy(message.createdAt.toLocal()).jm, + style: messageTheme.createdAtStyle, + ), + ), + if (showSendingIndicator) + WidgetSpan( + child: SendingIndicatorWrapper( + messageTheme: messageTheme, + message: message, + hasNonUrlAttachments: hasNonUrlAttachments, + streamChat: streamChat, + streamChatTheme: streamChatTheme, + ), + ), + ]); + + final showThreadTail = !(hasUrlAttachments || isGiphy || isOnlyEmoji) && + (showThreadReplyIndicator || showInChannel); + + final threadIndicatorWidgets = [ + if (showThreadTail) + WidgetSpan( + child: Padding( + padding: EdgeInsets.only( + bottom: context.textScaleFactor * + ((messageTheme.repliesStyle?.fontSize ?? 1) / 2), + ), + child: CustomPaint( + size: const Size(16, 32) * context.textScaleFactor, + painter: ThreadReplyPainter( + context: context, + color: messageTheme.messageBorderColor, + reverse: reverse, + ), + ), + ), + ), + if (showInChannel || showThreadReplyIndicator) ...[ + if (showThreadParticipants) + WidgetSpan( + child: SizedBox.fromSize( + size: Size((threadParticipants!.length * 8.0) + 8, 16), + child: ThreadParticipants( + threadParticipants: threadParticipants, + streamChatTheme: streamChatTheme, + ), + ), + ), + WidgetSpan( + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: _onThreadTap, + child: Text(msg, style: messageTheme.repliesStyle), + ), + ), + ), + ], + ]; + + if (reverse) { + children.addAll(threadIndicatorWidgets.reversed); + } else { + children.insertAll(0, threadIndicatorWidgets); + } + + return Text.rich( + TextSpan( + children: [ + ...children, + ].insertBetween(const WidgetSpan(child: SizedBox(width: 8))), + ), + maxLines: 1, + textAlign: reverse ? TextAlign.right : TextAlign.left, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/deleted_message.dart b/packages/stream_chat_flutter/lib/src/message_widget/deleted_message.dart similarity index 80% rename from packages/stream_chat_flutter/lib/src/deleted_message.dart rename to packages/stream_chat_flutter/lib/src/message_widget/deleted_message.dart index 0e47f903..3db36ef3 100644 --- a/packages/stream_chat_flutter/lib/src/deleted_message.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/deleted_message.dart @@ -1,17 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/theme/themes.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; -/// {@macro deleted_message} -@Deprecated("Use 'StreamDeletedMessage' instead") -typedef DeletedMessage = StreamDeletedMessage; - -/// {@template deleted_message} -/// Widget to display deleted message. +/// {@template streamDeletedMessage} +/// Displays that a message was deleted at this position in the message list. /// {@endtemplate} class StreamDeletedMessage extends StatelessWidget { - /// Constructor to create [StreamDeletedMessage] + /// {@macro streamDeletedMessage} const StreamDeletedMessage({ super.key, required this.messageTheme, @@ -30,7 +26,7 @@ class StreamDeletedMessage extends StatelessWidget { /// The shape of the message text final ShapeBorder? shape; - /// The borderside of the message text + /// The [BorderSide] of the message text final BorderSide? borderSide; /// If true the widget will be mirrored diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_card.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_card.dart new file mode 100644 index 00000000..8771b626 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_card.dart @@ -0,0 +1,232 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template messageCard} +/// The widget containing a quoted message. +/// +/// Used in [MessageWidgetContent]. Should not be used elsewhere. +/// {@endtemplate} +class MessageCard extends StatefulWidget { + /// {@macro messageCard} + const MessageCard({ + super.key, + required this.message, + required this.isFailedState, + required this.showUserAvatar, + required this.messageTheme, + required this.hasQuotedMessage, + required this.hasUrlAttachments, + required this.hasNonUrlAttachments, + required this.isOnlyEmoji, + required this.isGiphy, + required this.attachmentBuilders, + required this.attachmentPadding, + required this.textPadding, + required this.reverse, + this.shape, + this.borderSide, + this.borderRadiusGeometry, + this.textBuilder, + this.onLinkTap, + this.onMentionTap, + this.onQuotedMessageTap, + }); + + /// {@macro isFailedState} + final bool isFailedState; + + /// {@macro showUserAvatar} + final DisplayWidget showUserAvatar; + + /// {@macro shape} + final ShapeBorder? shape; + + /// {@macro borderSide} + final BorderSide? borderSide; + + /// {@macro messageTheme} + final StreamMessageThemeData messageTheme; + + /// {@macro borderRadiusGeometry} + final BorderRadiusGeometry? borderRadiusGeometry; + + /// {@macro hasQuotedMessage} + final bool hasQuotedMessage; + + /// {@macro hasUrlAttachments} + final bool hasUrlAttachments; + + /// {@macro hasNonUrlAttachments} + final bool hasNonUrlAttachments; + + /// {@macro isOnlyEmoji} + final bool isOnlyEmoji; + + /// {@macro isGiphy} + final bool isGiphy; + + /// {@macro message} + final Message message; + + /// {@macro attachmentBuilders} + final Map attachmentBuilders; + + /// {@macro attachmentPadding} + final EdgeInsetsGeometry attachmentPadding; + + /// {@macro textPadding} + final EdgeInsets textPadding; + + /// {@macro textBuilder} + final Widget Function(BuildContext, Message)? textBuilder; + + /// {@macro onLinkTap} + final void Function(String)? onLinkTap; + + /// {@macro onMentionTap} + final void Function(User)? onMentionTap; + + /// {@macro onQuotedMessageTap} + final OnQuotedMessageTap? onQuotedMessageTap; + + /// {@macro reverse} + final bool reverse; + + @override + State createState() => _MessageCardState(); +} + +class _MessageCardState extends State { + final GlobalKey attachmentsKey = GlobalKey(); + final GlobalKey linksKey = GlobalKey(); + double? widthLimit; + + @override + void initState() { + WidgetsBinding.instance.addPostFrameCallback((_) { + final attachmentsRenderBox = + attachmentsKey.currentContext?.findRenderObject() as RenderBox?; + final attachmentsWidth = attachmentsRenderBox?.size.width; + + final linkRenderBox = + linksKey.currentContext?.findRenderObject() as RenderBox?; + final linkWidth = linkRenderBox?.size.width; + + if (mounted) { + setState(() { + if (attachmentsWidth != null && linkWidth != null) { + widthLimit = max(attachmentsWidth, linkWidth); + } else { + widthLimit = attachmentsWidth ?? linkWidth; + } + }); + } + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Card( + elevation: 0, + margin: EdgeInsets.symmetric( + horizontal: (widget.isFailedState ? 15.0 : 0.0) + + (widget.showUserAvatar == DisplayWidget.gone ? 0 : 4.0), + ), + shape: widget.shape ?? + RoundedRectangleBorder( + side: widget.borderSide ?? + BorderSide( + color: widget.messageTheme.messageBorderColor ?? Colors.grey, + ), + borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero, + ), + color: _getBackgroundColor(), + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: widthLimit ?? double.infinity, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.hasQuotedMessage) + QuotedMessage( + reverse: widget.reverse, + message: widget.message, + hasNonUrlAttachments: widget.hasNonUrlAttachments, + onQuotedMessageTap: widget.onQuotedMessageTap, + ), + if (widget.hasNonUrlAttachments) + ParseAttachments( + key: attachmentsKey, + message: widget.message, + attachmentBuilders: widget.attachmentBuilders, + attachmentPadding: widget.attachmentPadding, + ), + if (!widget.isGiphy) + ConstrainedBox( + constraints: BoxConstraints.loose(const Size.fromWidth(500)), + child: TextBubble( + messageTheme: widget.messageTheme, + message: widget.message, + textPadding: widget.textPadding, + textBuilder: widget.textBuilder, + isOnlyEmoji: widget.isOnlyEmoji, + hasQuotedMessage: widget.hasQuotedMessage, + hasUrlAttachments: widget.hasUrlAttachments, + onLinkTap: widget.onLinkTap, + onMentionTap: widget.onMentionTap, + ), + ), + if (widget.hasUrlAttachments && !widget.hasQuotedMessage) + _buildUrlAttachment(), + ], + ), + ), + ); + } + + Widget _buildUrlAttachment() { + final urlAttachment = widget.message.attachments + .firstWhere((element) => element.titleLink != null); + + final host = Uri.parse(urlAttachment.titleLink!).host; + final splitList = host.split('.'); + final hostName = splitList.length == 3 ? splitList[1] : splitList[0]; + final hostDisplayName = urlAttachment.authorName?.capitalize() ?? + getWebsiteName(hostName.toLowerCase()) ?? + hostName.capitalize(); + + return StreamUrlAttachment( + key: linksKey, + urlAttachment: urlAttachment, + hostDisplayName: hostDisplayName, + textPadding: widget.textPadding, + messageTheme: widget.messageTheme, + ); + } + + Color? _getBackgroundColor() { + if (widget.hasQuotedMessage) { + return widget.messageTheme.messageBackgroundColor; + } + + if (widget.hasUrlAttachments) { + return widget.messageTheme.linkBackgroundColor; + } + + if (widget.isOnlyEmoji) { + return Colors.transparent; + } + + if (widget.isGiphy) { + return Colors.transparent; + } + + return widget.messageTheme.messageBackgroundColor; + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_text.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_text.dart similarity index 87% rename from packages/stream_chat_flutter/lib/src/message_text.dart rename to packages/stream_chat_flutter/lib/src/message_widget/message_text.dart index bb45aaa1..93dc0936 100644 --- a/packages/stream_chat_flutter/lib/src/message_text.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_text.dart @@ -1,18 +1,14 @@ import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@macro message_text} -@Deprecated("Use 'StreamMessageText' instead") -typedef MessageText = StreamMessageText; - -/// {@template message_text} -/// Text widget to display in message +/// {@template streamMessageText} +/// The text content of a message. /// {@endtemplate} class StreamMessageText extends StatelessWidget { - /// Constructor for creating a [StreamMessageText] widget + /// {@macro streamMessageText} const StreamMessageText({ super.key, required this.message, @@ -24,10 +20,10 @@ class StreamMessageText extends StatelessWidget { /// Message whose text is to be displayed final Message message; - /// Callback for when mention is tapped + /// The action to perform when a mention is tapped final void Function(User)? onMentionTap; - /// Callback for when link is tapped + /// The action to perform when a link is tapped final void Function(String)? onLinkTap; /// [StreamMessageThemeData] whose text theme is to be applied @@ -49,6 +45,7 @@ class StreamMessageText extends StatelessWidget { final themeData = Theme.of(context); return MarkdownBody( data: messageText ?? '', + selectable: isDesktopDeviceOrWeb, onTapLink: ( String link, String? href, diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart new file mode 100644 index 00000000..62126248 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart @@ -0,0 +1,1109 @@ +import 'package:contextmenu/contextmenu.dart'; +import 'package:flutter/material.dart' hide ButtonStyle; +import 'package:flutter/services.dart'; +import 'package:flutter_portal/flutter_portal.dart'; +import 'package:stream_chat_flutter/conditional_parent_builder/conditional_parent_builder.dart'; +import 'package:stream_chat_flutter/platform_widget_builder/platform_widget_builder.dart'; +import 'package:stream_chat_flutter/src/context_menu_items/context_menu_reaction_picker.dart'; +import 'package:stream_chat_flutter/src/context_menu_items/stream_chat_context_menu_item.dart'; +import 'package:stream_chat_flutter/src/dialogs/dialogs.dart'; +import 'package:stream_chat_flutter/src/message_actions_modal/message_actions_modal.dart'; +import 'package:stream_chat_flutter/src/message_widget/message_widget_content.dart'; +import 'package:stream_chat_flutter/src/message_widget/reactions/message_reactions_modal.dart'; +import 'package:stream_chat_flutter/src/utils/utils.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// The display behaviour of a widget +enum DisplayWidget { + /// Hides the widget replacing its space with a spacer + hide, + + /// Hides the widget not replacing its space + gone, + + /// Shows the widget normally + show, +} + +/// {@template messageWidget} +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_widget.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_widget_paint.png) +/// +/// Shows a message with reactions, replies and user avatar. +/// +/// Usually you don't use this widget as it's the default message widget used by +/// [MessageListView]. +/// +/// The widget components render the ui based on the first ancestor of type +/// [StreamChatTheme]. +/// Modify it to change the widget appearance. +/// {@endtemplate} +class StreamMessageWidget extends StatefulWidget { + /// {@macro messageWidget} + StreamMessageWidget({ + super.key, + required this.message, + required this.messageTheme, + this.reverse = false, + this.translateUserAvatar = true, + this.shape, + this.attachmentShape, + this.borderSide, + this.attachmentBorderSide, + this.borderRadiusGeometry, + this.attachmentBorderRadiusGeometry, + this.onMentionTap, + this.onMessageTap, + this.showReactionPickerIndicator = false, + this.showUserAvatar = DisplayWidget.show, + this.showSendingIndicator = true, + this.showThreadReplyIndicator = false, + this.showInChannelIndicator = false, + this.onReplyTap, + this.onThreadTap, + this.showUsername = true, + this.showTimestamp = true, + this.showReactions = true, + this.showDeleteMessage = true, + this.showEditMessage = true, + this.showReplyMessage = true, + this.showThreadReplyMessage = true, + this.showResendMessage = true, + this.showCopyMessage = true, + this.showFlagButton = true, + this.showPinButton = true, + this.showPinHighlight = true, + this.onUserAvatarTap, + this.onLinkTap, + this.onMessageActions, + this.onShowMessage, + this.userAvatarBuilder, + this.editMessageInputBuilder, + this.textBuilder, + this.bottomRowBuilder, + this.deletedBottomRowBuilder, + this.customAttachmentBuilders, + this.padding, + this.textPadding = const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + this.attachmentPadding = EdgeInsets.zero, + this.onQuotedMessageTap, + this.customActions = const [], + this.onAttachmentTap, + this.usernameBuilder, + this.imageAttachmentThumbnailSize = const Size(400, 400), + this.imageAttachmentThumbnailResizeType = 'clip', + this.imageAttachmentThumbnailCropType = 'center', + }) : attachmentBuilders = { + 'image': (context, message, attachments) { + final border = RoundedRectangleBorder( + borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero, + ); + + final mediaQueryData = MediaQuery.of(context); + if (attachments.length > 1) { + return Padding( + padding: attachmentPadding, + child: WrapAttachmentWidget( + attachmentWidget: Material( + color: messageTheme.messageBackgroundColor, + child: StreamImageGroup( + constraints: BoxConstraints( + maxWidth: 400, + minWidth: 400, + maxHeight: mediaQueryData.size.height * 0.3, + ), + images: attachments, + message: message, + messageTheme: messageTheme, + onShowMessage: onShowMessage, + onReplyMessage: onReplyTap, + onAttachmentTap: onAttachmentTap, + imageThumbnailSize: imageAttachmentThumbnailSize, + imageThumbnailResizeType: + imageAttachmentThumbnailResizeType, + imageThumbnailCropType: imageAttachmentThumbnailCropType, + ), + ), + attachmentShape: border, + reverse: reverse, + ), + ); + } + + return WrapAttachmentWidget( + attachmentWidget: StreamImageAttachment( + attachment: attachments[0], + message: message, + messageTheme: messageTheme, + constraints: BoxConstraints( + maxWidth: 400, + minWidth: 400, + maxHeight: mediaQueryData.size.height * 0.3, + ), + onShowMessage: onShowMessage, + onReplyMessage: onReplyTap, + onAttachmentTap: onAttachmentTap != null + ? () { + onAttachmentTap.call(message, attachments[0]); + } + : null, + imageThumbnailSize: imageAttachmentThumbnailSize, + imageThumbnailResizeType: imageAttachmentThumbnailResizeType, + imageThumbnailCropType: imageAttachmentThumbnailCropType, + ), + attachmentShape: border, + reverse: reverse, + ); + }, + 'video': (context, message, attachments) { + final border = RoundedRectangleBorder( + borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero, + ); + + return WrapAttachmentWidget( + attachmentWidget: Column( + children: attachments.map((attachment) { + final mediaQueryData = MediaQuery.of(context); + return StreamVideoAttachment( + attachment: attachment, + messageTheme: messageTheme, + constraints: BoxConstraints( + maxWidth: 400, + minWidth: 400, + maxHeight: mediaQueryData.size.height * 0.3, + ), + message: message, + onShowMessage: onShowMessage, + onReplyMessage: onReplyTap, + onAttachmentTap: onAttachmentTap != null + ? () { + onAttachmentTap(message, attachment); + } + : null, + ); + }).toList(), + ), + attachmentShape: border, + reverse: reverse, + ); + }, + 'giphy': (context, message, attachments) { + final border = RoundedRectangleBorder( + borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero, + ); + + return WrapAttachmentWidget( + attachmentWidget: Column( + children: attachments.map((attachment) { + final mediaQueryData = MediaQuery.of(context); + return StreamGiphyAttachment( + attachment: attachment, + message: message, + constraints: BoxConstraints( + maxWidth: 400, + minWidth: 400, + maxHeight: mediaQueryData.size.height * 0.3, + ), + onShowMessage: onShowMessage, + onReplyMessage: onReplyTap, + onAttachmentTap: onAttachmentTap != null + ? () { + onAttachmentTap(message, attachment); + } + : null, + ); + }).toList(), + ), + attachmentShape: border, + reverse: reverse, + ); + }, + 'file': (context, message, attachments) { + final border = RoundedRectangleBorder( + side: attachmentBorderSide ?? + BorderSide( + color: StreamChatTheme.of(context).colorTheme.borders, + ), + borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero, + ); + + return Column( + children: attachments + .map((attachment) { + final mediaQueryData = MediaQuery.of(context); + return WrapAttachmentWidget( + attachmentWidget: StreamFileAttachment( + message: message, + attachment: attachment, + constraints: BoxConstraints( + maxWidth: 400, + minWidth: 400, + maxHeight: mediaQueryData.size.height * 0.3, + ), + onAttachmentTap: onAttachmentTap != null + ? () { + onAttachmentTap(message, attachment); + } + : null, + ), + attachmentShape: border, + reverse: reverse, + ); + }) + .insertBetween(SizedBox( + height: attachmentPadding.vertical / 2, + )) + .toList(), + ); + }, + }..addAll(customAttachmentBuilders ?? {}); + + /// {@template onMentionTap} + /// Function called on mention tap + /// {@endtemplate} + final void Function(User)? onMentionTap; + + /// {@template onThreadTap} + /// The function called when tapping on threads + /// {@endtemplate} + final void Function(Message)? onThreadTap; + + /// {@template onReplyTap} + /// The function called when tapping on replies + /// {@endtemplate} + final void Function(Message)? onReplyTap; + + /// {@template editMessageInputBuilder} + /// Widget builder for edit message layout + /// {@endtemplate} + final Widget Function(BuildContext, Message)? editMessageInputBuilder; + + /// {@template textBuilder} + /// Widget builder for building text + /// {@endtemplate} + final Widget Function(BuildContext, Message)? textBuilder; + + /// {@template usernameBuilder} + /// Widget builder for building username + /// {@endtemplate} + final Widget Function(BuildContext, Message)? usernameBuilder; + + /// {@template onMessageActions} + /// Function called on long press + /// {@endtemplate} + final void Function(BuildContext, Message)? onMessageActions; + + /// {@template bottomRowBuilder} + /// Widget builder for building a bottom row below the message + /// {@endtemplate} + final Widget Function(BuildContext, Message)? bottomRowBuilder; + + /// {@template deletedBottomRowBuilder} + /// Widget builder for building a bottom row below a deleted message + /// {@endtemplate} + final Widget Function(BuildContext, Message)? deletedBottomRowBuilder; + + /// {@template userAvatarBuilder} + /// Widget builder for building user avatar + /// {@endtemplate} + final Widget Function(BuildContext, User)? userAvatarBuilder; + + /// {@template message} + /// The message to display. + /// {@endtemplate} + final Message message; + + /// {@template messageTheme} + /// The message theme + /// {@endtemplate} + final StreamMessageThemeData messageTheme; + + /// {@template reverse} + /// If true the widget will be mirrored + /// {@endtemplate} + final bool reverse; + + /// {@template shape} + /// The shape of the message text + /// {@endtemplate} + final ShapeBorder? shape; + + /// {@template attachmentShape} + /// The shape of an attachment + /// {@endtemplate} + final ShapeBorder? attachmentShape; + + /// {@template borderSide} + /// The borderSide of the message text + /// {@endtemplate} + final BorderSide? borderSide; + + /// {@template attachmentBorderSide} + /// The borderSide of an attachment + /// {@endtemplate} + final BorderSide? attachmentBorderSide; + + /// {@template borderRadiusGeometry} + /// The border radius of the message text + /// {@endtemplate} + final BorderRadiusGeometry? borderRadiusGeometry; + + /// {@template attachmentBorderRadiusGeometry} + /// The border radius of an attachment + /// {@endtemplate} + final BorderRadiusGeometry? attachmentBorderRadiusGeometry; + + /// {@template padding} + /// The padding of the widget + /// {@endtemplate} + final EdgeInsetsGeometry? padding; + + /// {@template textPadding} + /// The internal padding of the message text + /// {@endtemplate} + final EdgeInsets textPadding; + + /// {@template attachmentPadding} + /// The internal padding of an attachment + /// {@endtemplate} + final EdgeInsetsGeometry attachmentPadding; + + /// {@template showUserAvatar} + /// It controls the display behaviour of the user avatar + /// {@endtemplate} + final DisplayWidget showUserAvatar; + + /// {@template showSendingIndicator} + /// It controls the display behaviour of the sending indicator + /// {@endtemplate} + final bool showSendingIndicator; + + /// {@template showReactions} + /// If `true` the message's reactions will be shown. + /// {@endtemplate} + final bool showReactions; + + /// {@template showThreadReplyIndicator} + /// If true the widget will show the thread reply indicator + /// {@endtemplate} + final bool showThreadReplyIndicator; + + /// {@template showInChannelIndicator} + /// If true the widget will show the show in channel indicator + /// {@endtemplate} + final bool showInChannelIndicator; + + /// {@template onUserAvatarTap} + /// The function called when tapping on UserAvatar + /// {@endtemplate} + final void Function(User)? onUserAvatarTap; + + /// {@template onLinkTap} + /// The function called when tapping on a link + /// {@endtemplate} + final void Function(String)? onLinkTap; + + /// {@template showReactionPickerIndicator} + /// Used in [StreamMessageReactionsModal] and [MessageActionsModal] + /// {@endtemplate} + final bool showReactionPickerIndicator; + + /// {@template onShowMessage} + /// Callback when show message is tapped + /// {@endtemplate} + final ShowMessageCallback? onShowMessage; + + /// {@template showUsername} + /// If true show the users username next to the timestamp of the message + /// {@endtemplate} + final bool showUsername; + + /// {@template showTimestamp} + /// Show message timestamp + /// {@endtemplate} + final bool showTimestamp; + + /// {@template showReplyMessage} + /// Show reply action + /// {@endtemplate} + final bool showReplyMessage; + + /// {@template showThreadReplyMessage} + /// Show thread reply action + /// {@endtemplate} + final bool showThreadReplyMessage; + + /// {@template showEditMessage} + /// Show edit action + /// {@endtemplate} + final bool showEditMessage; + + /// {@template showCopyMessage} + /// Show copy action + /// {@endtemplate} + final bool showCopyMessage; + + /// {@template showDeleteMessage} + /// Show delete action + /// {@endtemplate} + final bool showDeleteMessage; + + /// {@template showResendMessage} + /// Show resend action + /// {@endtemplate} + final bool showResendMessage; + + /// {@template showFlagButton} + /// Show flag action + /// {@endtemplate} + final bool showFlagButton; + + /// {@template showPinButton} + /// Show pin action + /// {@endtemplate} + final bool showPinButton; + + /// {@template showPinHighlight} + /// Display Pin Highlight + /// {@endtemplate} + final bool showPinHighlight; + + /// {@template attachmentBuilders} + /// Builder for respective attachment types + /// {@endtemplate} + final Map attachmentBuilders; + + /// {@template customAttachmentBuilders} + /// Builder for respective attachment types (user facing builder) + /// {@endtemplate} + final Map? customAttachmentBuilders; + + /// {@template translateUserAvatar} + /// Center user avatar with bottom of the message + /// {@endtemplate} + final bool translateUserAvatar; + + /// {@macro onQuotedMessageTap} + final OnQuotedMessageTap? onQuotedMessageTap; + + /// {@macro onMessageTap} + final void Function(Message)? onMessageTap; + + /// {@template customActions} + /// List of custom actions shown on message long tap + /// {@endtemplate} + final List customActions; + + /// {@macro onMessageWidgetAttachmentTap} + final OnMessageWidgetAttachmentTap? onAttachmentTap; + + /// Size of the image attachment thumbnail. + final Size imageAttachmentThumbnailSize; + + /// Resize type of the image attachment thumbnail. + /// + /// Defaults to [crop] + final String /*clip|crop|scale|fill*/ imageAttachmentThumbnailResizeType; + + /// Crop type of the image attachment thumbnail. + /// + /// Defaults to [center] + final String /*center|top|bottom|left|right*/ + imageAttachmentThumbnailCropType; + + /// {@template copyWith} + /// Creates a copy of [StreamMessageWidget] with specified attributes + /// overridden. + /// {@endtemplate} + StreamMessageWidget copyWith({ + Key? key, + void Function(User)? onMentionTap, + void Function(Message)? onThreadTap, + void Function(Message)? onReplyTap, + Widget Function(BuildContext, Message)? editMessageInputBuilder, + Widget Function(BuildContext, Message)? textBuilder, + Widget Function(BuildContext, Message)? usernameBuilder, + Widget Function(BuildContext, Message)? bottomRowBuilder, + Widget Function(BuildContext, Message)? deletedBottomRowBuilder, + void Function(BuildContext, Message)? onMessageActions, + Message? message, + StreamMessageThemeData? messageTheme, + bool? reverse, + ShapeBorder? shape, + ShapeBorder? attachmentShape, + BorderSide? borderSide, + BorderSide? attachmentBorderSide, + BorderRadiusGeometry? borderRadiusGeometry, + BorderRadiusGeometry? attachmentBorderRadiusGeometry, + EdgeInsetsGeometry? padding, + EdgeInsets? textPadding, + EdgeInsetsGeometry? attachmentPadding, + DisplayWidget? showUserAvatar, + bool? showSendingIndicator, + bool? showReactions, + bool? allRead, + bool? showThreadReplyIndicator, + bool? showInChannelIndicator, + void Function(User)? onUserAvatarTap, + void Function(String)? onLinkTap, + bool? showReactionPickerIndicator, + List? readList, + ShowMessageCallback? onShowMessage, + bool? showUsername, + bool? showTimestamp, + bool? showReplyMessage, + bool? showThreadReplyMessage, + bool? showEditMessage, + bool? showCopyMessage, + bool? showDeleteMessage, + bool? showResendMessage, + bool? showFlagButton, + bool? showPinButton, + bool? showPinHighlight, + Map? customAttachmentBuilders, + bool? translateUserAvatar, + OnQuotedMessageTap? onQuotedMessageTap, + void Function(Message)? onMessageTap, + List? customActions, + void Function(Message message, Attachment attachment)? onAttachmentTap, + Widget Function(BuildContext, User)? userAvatarBuilder, + Size? imageAttachmentThumbnailSize, + String? imageAttachmentThumbnailResizeType, + String? imageAttachmentThumbnailCropType, + }) { + return StreamMessageWidget( + key: key ?? this.key, + onMentionTap: onMentionTap ?? this.onMentionTap, + onThreadTap: onThreadTap ?? this.onThreadTap, + onReplyTap: onReplyTap ?? this.onReplyTap, + editMessageInputBuilder: + editMessageInputBuilder ?? this.editMessageInputBuilder, + textBuilder: textBuilder ?? this.textBuilder, + usernameBuilder: usernameBuilder ?? this.usernameBuilder, + bottomRowBuilder: bottomRowBuilder ?? this.bottomRowBuilder, + deletedBottomRowBuilder: + deletedBottomRowBuilder ?? this.deletedBottomRowBuilder, + onMessageActions: onMessageActions ?? this.onMessageActions, + message: message ?? this.message, + messageTheme: messageTheme ?? this.messageTheme, + reverse: reverse ?? this.reverse, + shape: shape ?? this.shape, + attachmentShape: attachmentShape ?? this.attachmentShape, + borderSide: borderSide ?? this.borderSide, + attachmentBorderSide: attachmentBorderSide ?? this.attachmentBorderSide, + borderRadiusGeometry: borderRadiusGeometry ?? this.borderRadiusGeometry, + attachmentBorderRadiusGeometry: + attachmentBorderRadiusGeometry ?? this.attachmentBorderRadiusGeometry, + padding: padding ?? this.padding, + textPadding: textPadding ?? this.textPadding, + attachmentPadding: attachmentPadding ?? this.attachmentPadding, + showUserAvatar: showUserAvatar ?? this.showUserAvatar, + showSendingIndicator: showSendingIndicator ?? this.showSendingIndicator, + showReactions: showReactions ?? this.showReactions, + showThreadReplyIndicator: + showThreadReplyIndicator ?? this.showThreadReplyIndicator, + showInChannelIndicator: + showInChannelIndicator ?? this.showInChannelIndicator, + onUserAvatarTap: onUserAvatarTap ?? this.onUserAvatarTap, + onLinkTap: onLinkTap ?? this.onLinkTap, + showReactionPickerIndicator: + showReactionPickerIndicator ?? this.showReactionPickerIndicator, + onShowMessage: onShowMessage ?? this.onShowMessage, + showUsername: showUsername ?? this.showUsername, + showTimestamp: showTimestamp ?? this.showTimestamp, + showReplyMessage: showReplyMessage ?? this.showReplyMessage, + showThreadReplyMessage: + showThreadReplyMessage ?? this.showThreadReplyMessage, + showEditMessage: showEditMessage ?? this.showEditMessage, + showCopyMessage: showCopyMessage ?? this.showCopyMessage, + showDeleteMessage: showDeleteMessage ?? this.showDeleteMessage, + showResendMessage: showResendMessage ?? this.showResendMessage, + showFlagButton: showFlagButton ?? this.showFlagButton, + showPinButton: showPinButton ?? this.showPinButton, + showPinHighlight: showPinHighlight ?? this.showPinHighlight, + customAttachmentBuilders: + customAttachmentBuilders ?? this.customAttachmentBuilders, + translateUserAvatar: translateUserAvatar ?? this.translateUserAvatar, + onQuotedMessageTap: onQuotedMessageTap ?? this.onQuotedMessageTap, + onMessageTap: onMessageTap ?? this.onMessageTap, + customActions: customActions ?? this.customActions, + onAttachmentTap: onAttachmentTap ?? this.onAttachmentTap, + userAvatarBuilder: userAvatarBuilder ?? this.userAvatarBuilder, + imageAttachmentThumbnailSize: + imageAttachmentThumbnailSize ?? this.imageAttachmentThumbnailSize, + imageAttachmentThumbnailResizeType: imageAttachmentThumbnailResizeType ?? + this.imageAttachmentThumbnailResizeType, + imageAttachmentThumbnailCropType: imageAttachmentThumbnailCropType ?? + this.imageAttachmentThumbnailCropType, + ); + } + + @override + _StreamMessageWidgetState createState() => _StreamMessageWidgetState(); +} + +class _StreamMessageWidgetState extends State + with AutomaticKeepAliveClientMixin { + bool get showThreadReplyIndicator => widget.showThreadReplyIndicator; + + bool get showSendingIndicator => widget.showSendingIndicator; + + bool get isDeleted => widget.message.isDeleted; + + bool get showUsername => widget.showUsername; + + bool get showTimeStamp => widget.showTimestamp; + + bool get showInChannel => widget.showInChannelIndicator; + + /// {@template hasQuotedMessage} + /// `true` if [StreamMessageWidget.quotedMessage] is not null. + /// {@endtemplate} + bool get hasQuotedMessage => widget.message.quotedMessage != null; + + bool get isSendFailed => widget.message.status == MessageSendingStatus.failed; + + bool get isUpdateFailed => + widget.message.status == MessageSendingStatus.failed_update; + + bool get isDeleteFailed => + widget.message.status == MessageSendingStatus.failed_delete; + + /// {@template isFailedState} + /// Whether the message has failed to be sent, updated, or deleted. + /// {@endtemplate} + bool get isFailedState => isSendFailed || isUpdateFailed || isDeleteFailed; + + /// {@template isGiphy} + /// `true` if any of the [message]'s attachments are a giphy. + /// {@endtemplate} + bool get isGiphy => + widget.message.attachments.any((element) => element.type == 'giphy'); + + /// {@template isOnlyEmoji} + /// `true` if [message.text] contains only emoji. + /// {@endtemplate} + bool get isOnlyEmoji => widget.message.text?.isOnlyEmoji == true; + + /// {@template hasNonUrlAttachments} + /// `true` if any of the [message]'s attachments are a giphy and do not + /// have a [Attachment.titleLink]. + /// {@endtemplate} + bool get hasNonUrlAttachments => widget.message.attachments + .where((it) => it.titleLink == null || it.type == 'giphy') + .isNotEmpty; + + /// {@template hasUrlAttachments} + /// `true` if any of the [message]'s attachments are a giphy with a + /// [Attachment.titleLink]. + /// {@endtemplate} + bool get hasUrlAttachments => widget.message.attachments + .any((it) => it.titleLink != null && it.type != 'giphy'); + + /// {@template showBottomRow} + /// Show the [BottomRow] widget if any of the following are `true`: + /// * [StreamMessageWidget.showThreadReplyIndicator] + /// * [StreamMessageWidget.showUsername] + /// * [StreamMessageWidget.showTimestamp] + /// * [StreamMessageWidget.showInChannelIndicator] + /// * [StreamMessageWidget.showSendingIndicator] + /// * [StreamMessageWidget.message.isDeleted] + /// {@endtemplate} + bool get showBottomRow => + showThreadReplyIndicator || + showUsername || + showTimeStamp || + showInChannel || + showSendingIndicator || + isDeleted; + + /// {@template isPinned} + /// Whether [StreamMessageWidget.message] is pinned or not. + /// {@endtemplate} + bool get isPinned => widget.message.pinned; + + /// {@template shouldShowReactions} + /// Should show message reactions if [StreamMessageWidget.showReactions] is + /// `true`, if there are reactions to show, and if the message is not deleted. + /// {@endtemplate} + bool get shouldShowReactions => + widget.showReactions && + (widget.message.reactionCounts?.isNotEmpty == true) && + !widget.message.isDeleted; + + bool get shouldShowReplyAction => + widget.showReplyMessage && !isFailedState && widget.onReplyTap != null; + + bool get shouldShowEditAction => + widget.showEditMessage && + !isDeleteFailed && + !widget.message.attachments.any((element) => element.type == 'giphy'); + + bool get shouldShowResendAction => + widget.showResendMessage && (isSendFailed || isUpdateFailed); + + bool get shouldShowCopyAction => + widget.showCopyMessage && + !isFailedState && + widget.message.text?.trim().isNotEmpty == true; + + bool get shouldShowEditMessage => + widget.showEditMessage && + !isDeleteFailed && + !widget.message.attachments.any((element) => element.type == 'giphy'); + + bool get shouldShowThreadReplyAction => + widget.showThreadReplyMessage && + !isFailedState && + widget.onThreadTap != null; + + bool get shouldShowDeleteAction => widget.showDeleteMessage || isDeleteFailed; + + @override + bool get wantKeepAlive => widget.message.attachments.isNotEmpty; + + late StreamChatThemeData _streamChatTheme; + late StreamChatState _streamChat; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _streamChatTheme = StreamChatTheme.of(context); + _streamChat = StreamChat.of(context); + } + + @override + Widget build(BuildContext context) { + super.build(context); + final avatarWidth = + widget.messageTheme.avatarTheme?.constraints.maxWidth ?? 40; + final bottomRowPadding = + widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5; + + final showReactions = shouldShowReactions; + + return ConditionalParentBuilder( + builder: (context, child) { + if (!widget.message.isDeleted) { + return ContextMenuArea( + verticalPadding: 0, + builder: (context) => _buildContextMenu(), + child: child, + ); + } else { + return child; + } + }, + child: Material( + type: MaterialType.transparency, + child: AnimatedContainer( + duration: const Duration(seconds: 1), + color: widget.message.pinned && widget.showPinHighlight + ? _streamChatTheme.colorTheme.highlight + : _streamChatTheme.colorTheme.barsBg.withOpacity(0), + child: Portal( + child: PlatformWidgetBuilder( + mobile: (context, child) { + return InkWell( + onTap: () => widget.onMessageTap!(widget.message), + onLongPress: widget.message.isDeleted && !isFailedState + ? null + : () => onLongPress(context), + child: child, + ); + }, + desktop: (_, child) => MouseRegion(child: child), + web: (_, child) => MouseRegion(child: child), + child: Padding( + padding: widget.padding ?? const EdgeInsets.all(8), + child: FractionallySizedBox( + alignment: widget.reverse + ? Alignment.centerRight + : Alignment.centerLeft, + widthFactor: 0.78, + child: MessageWidgetContent( + streamChatTheme: _streamChatTheme, + showUsername: showUsername, + showTimeStamp: showTimeStamp, + showThreadReplyIndicator: showThreadReplyIndicator, + showSendingIndicator: showSendingIndicator, + showInChannel: showInChannel, + isGiphy: isGiphy, + isOnlyEmoji: isOnlyEmoji, + hasUrlAttachments: hasUrlAttachments, + messageTheme: widget.messageTheme, + reverse: widget.reverse, + message: widget.message, + hasNonUrlAttachments: hasNonUrlAttachments, + shouldShowReactions: shouldShowReactions, + hasQuotedMessage: hasQuotedMessage, + textPadding: widget.textPadding, + attachmentBuilders: widget.attachmentBuilders, + attachmentPadding: widget.attachmentPadding, + avatarWidth: avatarWidth, + bottomRowPadding: bottomRowPadding, + isFailedState: isFailedState, + isPinned: isPinned, + messageWidget: widget, + showBottomRow: showBottomRow, + showPinHighlight: widget.showPinHighlight, + showReactionPickerIndicator: + widget.showReactionPickerIndicator, + showReactions: showReactions, + showUserAvatar: widget.showUserAvatar, + streamChat: _streamChat, + translateUserAvatar: widget.translateUserAvatar, + deletedBottomRowBuilder: widget.deletedBottomRowBuilder, + onThreadTap: widget.onThreadTap, + shape: widget.shape, + borderSide: widget.borderSide, + borderRadiusGeometry: widget.borderRadiusGeometry, + textBuilder: widget.textBuilder, + onLinkTap: widget.onLinkTap, + onMentionTap: widget.onMentionTap, + onQuotedMessageTap: widget.onQuotedMessageTap, + bottomRowBuilder: widget.bottomRowBuilder, + onUserAvatarTap: widget.onUserAvatarTap, + userAvatarBuilder: widget.userAvatarBuilder, + usernameBuilder: widget.usernameBuilder, + ), + ), + ), + ), + ), + ), + ), + ); + } + + List _buildContextMenu() { + final channel = StreamChannel.of(context).channel; + + return [ + StreamChatContextMenuItem( + child: StreamChannel( + channel: channel, + child: ContextMenuReactionPicker( + message: widget.message, + ), + ), + ), + if (shouldShowReplyAction) ...[ + StreamChatContextMenuItem( + leading: StreamSvgIcon.reply(), + title: Text(context.translations.replyLabel), + onClick: () { + Navigator.of(context, rootNavigator: true).pop(); + widget.onReplyTap!(widget.message); + }, + ), + ], + if (shouldShowThreadReplyAction) + StreamChatContextMenuItem( + leading: StreamSvgIcon.thread(), + title: Text(context.translations.threadReplyLabel), + onClick: () { + Navigator.of(context, rootNavigator: true).pop(); + widget.onThreadTap!(widget.message); + }, + ), + if (shouldShowCopyAction) + StreamChatContextMenuItem( + leading: StreamSvgIcon.copy(), + title: Text(context.translations.copyMessageLabel), + onClick: () { + Navigator.of(context, rootNavigator: true).pop(); + Clipboard.setData(ClipboardData(text: widget.message.text)); + }, + ), + if (shouldShowEditAction) ...[ + StreamChatContextMenuItem( + leading: StreamSvgIcon.edit(color: Colors.grey), + title: Text(context.translations.editMessageLabel), + onClick: () { + Navigator.of(context, rootNavigator: true).pop(); + showModalBottomSheet( + context: context, + elevation: 2, + clipBehavior: Clip.hardEdge, + isScrollControlled: true, + backgroundColor: + StreamMessageInputTheme.of(context).inputBackgroundColor, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + builder: (_) => EditMessageSheet( + message: widget.message, + channel: StreamChannel.of(context).channel, + ), + ); + }, + ), + ], + if (widget.showPinButton) + StreamChatContextMenuItem( + leading: StreamSvgIcon.pin( + color: Colors.grey, + size: 24, + ), + title: Text( + context.translations.togglePinUnpinText( + pinned: widget.message.pinned, + ), + ), + onClick: () async { + Navigator.of(context, rootNavigator: true).pop(); + try { + if (!widget.message.pinned) { + await channel.pinMessage(widget.message); + } else { + await channel.unpinMessage(widget.message); + } + } catch (e) { + throw Exception(e); + } + }, + ), + if (shouldShowResendAction) + StreamChatContextMenuItem( + leading: StreamSvgIcon.iconSendMessage(), + title: Text( + context.translations.toggleResendOrResendEditedMessage( + isUpdateFailed: + widget.message.status == MessageSendingStatus.failed, + ), + ), + onClick: () { + Navigator.of(context, rootNavigator: true).pop(); + final isUpdateFailed = + widget.message.status == MessageSendingStatus.failed_update; + final channel = StreamChannel.of(context).channel; + if (isUpdateFailed) { + channel.updateMessage(widget.message); + } else { + channel.sendMessage(widget.message); + } + }, + ), + if (shouldShowDeleteAction) + StreamChatContextMenuItem( + leading: StreamSvgIcon.delete(color: Colors.red), + title: Text( + context.translations.deleteMessageLabel, + style: const TextStyle(color: Colors.red), + ), + onClick: () async { + Navigator.of(context, rootNavigator: true).pop(); + final deleted = await showDialog( + context: context, + barrierDismissible: false, + builder: (_) => const DeleteMessageDialog(), + ); + if (deleted) { + try { + await StreamChannel.of(context) + .channel + .deleteMessage(widget.message); + } catch (e) { + showDialog( + context: context, + builder: (_) => const MessageDialog(), + ); + } + } + }, + ), + ]; + } + + void onLongPress(BuildContext context) { + if (widget.message.isEphemeral || + widget.message.status == MessageSendingStatus.sending) { + return; + } + + if (widget.onMessageActions != null) { + widget.onMessageActions!(context, widget.message); + } else { + _showMessageActionModalBottomSheet(context); + } + return; + } + + void _showMessageActionModalBottomSheet(BuildContext context) { + final channel = StreamChannel.of(context).channel; + + showDialog( + useRootNavigator: false, + context: context, + barrierColor: _streamChatTheme.colorTheme.overlay, + builder: (context) => StreamChannel( + channel: channel, + child: MessageActionsModal( + messageWidget: widget.copyWith( + key: const Key('MessageWidget'), + message: widget.message.copyWith( + text: (widget.message.text?.length ?? 0) > 200 + ? '${widget.message.text!.substring(0, 200)}...' + : widget.message.text, + ), + showReactions: false, + showUsername: false, + showTimestamp: false, + translateUserAvatar: false, + showSendingIndicator: false, + padding: EdgeInsets.zero, + showReactionPickerIndicator: widget.showReactions && + (widget.message.status == MessageSendingStatus.sent), + showPinHighlight: false, + showUserAvatar: + widget.message.user!.id == channel.client.state.currentUser!.id + ? DisplayWidget.gone + : DisplayWidget.show, + ), + onCopyTap: (message) => + Clipboard.setData(ClipboardData(text: message.text)), + messageTheme: widget.messageTheme, + reverse: widget.reverse, + showDeleteMessage: shouldShowDeleteAction, + message: widget.message, + editMessageInputBuilder: widget.editMessageInputBuilder, + onReplyTap: widget.onReplyTap, + onThreadReplyTap: widget.onThreadTap, + showResendMessage: shouldShowResendAction, + showCopyMessage: shouldShowCopyAction, + showEditMessage: shouldShowEditAction, + showReactions: widget.showReactions, + showReplyMessage: shouldShowReplyAction, + showThreadReplyMessage: shouldShowThreadReplyAction, + showFlagButton: widget.showFlagButton, + showPinButton: widget.showPinButton, + customActions: widget.customActions, + ), + ), + ); + } + + void retryMessage(BuildContext context) { + final channel = StreamChannel.of(context).channel; + if (widget.message.status == MessageSendingStatus.failed) { + channel.sendMessage(widget.message); + return; + } + if (widget.message.status == MessageSendingStatus.failed_update) { + channel.updateMessage(widget.message); + return; + } + + if (widget.message.status == MessageSendingStatus.failed_delete) { + channel.deleteMessage(widget.message); + return; + } + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart new file mode 100644 index 00000000..c7655608 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart @@ -0,0 +1,461 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_portal/flutter_portal.dart'; +import 'package:stream_chat_flutter/src/message_widget/message_widget_content_components.dart'; +import 'package:stream_chat_flutter/src/message_widget/reactions/desktop_reactions_builder.dart'; +import 'package:stream_chat_flutter/src/utils/utils.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template messageWidgetContent} +/// The main content of a [StreamMessageWidget]. +/// +/// Should not be used outside of [MessageWidget. +/// {@endtemplate} +class MessageWidgetContent extends StatelessWidget { + /// {@macro messageWidgetContent} + const MessageWidgetContent({ + super.key, + required this.reverse, + required this.isPinned, + required this.showPinHighlight, + required this.showBottomRow, + required this.message, + required this.showUserAvatar, + required this.avatarWidth, + required this.showReactions, + required this.messageTheme, + required this.shouldShowReactions, + required this.streamChatTheme, + required this.isFailedState, + required this.hasQuotedMessage, + required this.hasUrlAttachments, + required this.hasNonUrlAttachments, + required this.isOnlyEmoji, + required this.isGiphy, + required this.attachmentBuilders, + required this.attachmentPadding, + required this.textPadding, + required this.showReactionPickerIndicator, + required this.translateUserAvatar, + required this.bottomRowPadding, + required this.showInChannel, + required this.streamChat, + required this.showSendingIndicator, + required this.showThreadReplyIndicator, + required this.showTimeStamp, + required this.showUsername, + required this.messageWidget, + this.onUserAvatarTap, + this.borderRadiusGeometry, + this.borderSide, + this.shape, + this.onQuotedMessageTap, + this.onMentionTap, + this.onLinkTap, + this.textBuilder, + this.bottomRowBuilder, + this.onThreadTap, + this.deletedBottomRowBuilder, + this.userAvatarBuilder, + this.usernameBuilder, + }); + + /// {@macro reverse} + final bool reverse; + + /// {@macro isPinned} + final bool isPinned; + + /// {@macro showPinHighlight} + final bool showPinHighlight; + + /// {@macro showBottomRow} + final bool showBottomRow; + + /// {@macro message} + final Message message; + + /// {@macro showUserAvatar} + final DisplayWidget showUserAvatar; + + /// The width of the avatar. + final double avatarWidth; + + /// {@macro showReactions} + final bool showReactions; + + /// {@macro messageTheme} + final StreamMessageThemeData messageTheme; + + /// {@macro shouldShowReactions} + final bool shouldShowReactions; + + /// {@macro onUserAvatarTap} + final void Function(User)? onUserAvatarTap; + + /// {@macro streamChatThemeData} + final StreamChatThemeData streamChatTheme; + + /// {@macro isFailedState} + final bool isFailedState; + + /// {@macro borderRadiusGeometry} + final BorderRadiusGeometry? borderRadiusGeometry; + + /// {@macro borderSide} + final BorderSide? borderSide; + + /// {@macro shape} + final ShapeBorder? shape; + + /// {@macro hasQuotedMessage} + final bool hasQuotedMessage; + + /// {@macro hasUrlAttachments} + final bool hasUrlAttachments; + + /// {@macro hasNonUrlAttachments} + final bool hasNonUrlAttachments; + + /// {@macro isOnlyEmoji} + final bool isOnlyEmoji; + + /// {@macro isGiphy} + final bool isGiphy; + + /// {@macro attachmentBuilders} + final Map attachmentBuilders; + + /// {@macro attachmentPadding} + final EdgeInsetsGeometry attachmentPadding; + + /// {@macro textPadding} + final EdgeInsets textPadding; + + /// {@macro onQuotedMessageTap} + final OnQuotedMessageTap? onQuotedMessageTap; + + /// {@macro onMentionTap} + final void Function(User)? onMentionTap; + + /// {@macro onLinkTap} + final void Function(String)? onLinkTap; + + /// {@macro textBuilder} + final Widget Function(BuildContext, Message)? textBuilder; + + /// {@macro showReactionPickerIndicator} + final bool showReactionPickerIndicator; + + /// {@macro translateUserAvatar} + final bool translateUserAvatar; + + /// The padding to use for this widget. + final double bottomRowPadding; + + /// {@macro bottomRowBuilder} + final Widget Function(BuildContext, Message)? bottomRowBuilder; + + /// {@macro showInChannelIndicator} + final bool showInChannel; + + /// {@macro streamChat} + final StreamChatState streamChat; + + /// {@macro showSendingIndicator} + final bool showSendingIndicator; + + /// {@macro showThreadReplyIndicator} + final bool showThreadReplyIndicator; + + /// {@macro showTimestamp} + final bool showTimeStamp; + + /// {@macro showUsername} + final bool showUsername; + + /// {@macro onThreadTap} + final void Function(Message)? onThreadTap; + + /// {@macro deletedBottomRowBuilder} + final Widget Function(BuildContext, Message)? deletedBottomRowBuilder; + + /// {@macro messageWidget} + final StreamMessageWidget messageWidget; + + /// {@macro userAvatarBuilder} + final Widget Function(BuildContext, User)? userAvatarBuilder; + + /// {@macro usernameBuilder} + final Widget Function(BuildContext, Message)? usernameBuilder; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: + reverse ? CrossAxisAlignment.end : CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + clipBehavior: Clip.none, + alignment: reverse + ? AlignmentDirectional.bottomEnd + : AlignmentDirectional.bottomStart, + children: [ + if (showBottomRow) + Padding( + padding: EdgeInsets.only( + left: !reverse ? bottomRowPadding : 0, + right: reverse ? bottomRowPadding : 0, + bottom: isPinned && showPinHighlight ? 6.0 : 0.0, + ), + child: bottomRowBuilder?.call( + context, + message, + ) ?? + BottomRow( + message: message, + reverse: reverse, + messageTheme: messageTheme, + hasUrlAttachments: hasUrlAttachments, + isOnlyEmoji: isOnlyEmoji, + isDeleted: message.isDeleted, + isGiphy: isGiphy, + showInChannel: showInChannel, + showSendingIndicator: showSendingIndicator, + showThreadReplyIndicator: showThreadReplyIndicator, + showTimeStamp: showTimeStamp, + showUsername: showUsername, + streamChatTheme: streamChatTheme, + onThreadTap: onThreadTap, + deletedBottomRowBuilder: deletedBottomRowBuilder, + streamChat: streamChat, + hasNonUrlAttachments: hasNonUrlAttachments, + usernameBuilder: usernameBuilder, + ), + ), + Padding( + padding: EdgeInsets.only( + bottom: isPinned && showPinHighlight ? 8.0 : 0.0, + ), + child: Column( + crossAxisAlignment: + reverse ? CrossAxisAlignment.end : CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (message.pinned && + message.pinnedBy != null && + showPinHighlight) + PinnedMessage( + pinnedBy: message.pinnedBy!, + currentUser: streamChat.currentUser!, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + if (!reverse && + showUserAvatar == DisplayWidget.show && + message.user != null) ...[ + UserAvatarTransform( + onUserAvatarTap: onUserAvatarTap, + userAvatarBuilder: userAvatarBuilder, + translateUserAvatar: translateUserAvatar, + messageTheme: messageTheme, + message: message, + ), + const SizedBox(width: 4), + ], + if (showUserAvatar == DisplayWidget.hide) + SizedBox(width: avatarWidth + 4), + Flexible( + child: PortalTarget( + visible: isMobileDevice && showReactions, + portalFollower: isMobileDevice && showReactions + ? ReactionIndicator( + message: message, + messageTheme: messageTheme, + ownId: streamChat.currentUser!.id, + reverse: reverse, + shouldShowReactions: shouldShowReactions, + onTap: () => _showMessageReactionsModal( + context, + ), + ) + : null, + anchor: Aligned( + follower: Alignment( + reverse ? 1 : -1, + -1, + ), + target: Alignment( + reverse ? -1 : 1, + -1, + ), + ), + child: Stack( + clipBehavior: Clip.none, + children: [ + Padding( + padding: showReactions + ? EdgeInsets.only( + top: message.reactionCounts + ?.isNotEmpty == + true + ? 18 + : 0, + ) + : EdgeInsets.zero, + child: (message.isDeleted && !isFailedState) + ? Container( + // ignore: lines_longer_than_80_chars + margin: EdgeInsets.symmetric( + horizontal: + // ignore: lines_longer_than_80_chars + showUserAvatar == + // ignore: lines_longer_than_80_chars + DisplayWidget.gone + ? 0 + : 4.0, + ), + child: StreamDeletedMessage( + borderRadiusGeometry: + borderRadiusGeometry, + borderSide: borderSide, + shape: shape, + messageTheme: messageTheme, + ), + ) + : MessageCard( + message: message, + isFailedState: isFailedState, + showUserAvatar: showUserAvatar, + messageTheme: messageTheme, + hasQuotedMessage: hasQuotedMessage, + hasUrlAttachments: hasUrlAttachments, + hasNonUrlAttachments: + hasNonUrlAttachments, + isOnlyEmoji: isOnlyEmoji, + isGiphy: isGiphy, + attachmentBuilders: attachmentBuilders, + attachmentPadding: attachmentPadding, + textPadding: textPadding, + reverse: reverse, + onQuotedMessageTap: onQuotedMessageTap, + onMentionTap: onMentionTap, + onLinkTap: onLinkTap, + textBuilder: textBuilder, + borderRadiusGeometry: + borderRadiusGeometry, + borderSide: borderSide, + shape: shape, + ), + ), + if (showReactionPickerIndicator) + Positioned( + right: reverse ? null : 4, + left: reverse ? 4 : null, + top: -8, + child: CustomPaint( + painter: ReactionBubblePainter( + streamChatTheme.colorTheme.barsBg, + Colors.transparent, + Colors.transparent, + tailCirclesSpace: 1, + ), + ), + ), + ], + ), + ), + ), + if (reverse && + showUserAvatar == DisplayWidget.show && + message.user != null) ...[ + UserAvatarTransform( + translateUserAvatar: translateUserAvatar, + messageTheme: messageTheme, + message: message, + ), + const SizedBox(width: 4), + ], + if (showUserAvatar == DisplayWidget.hide) + SizedBox(width: avatarWidth + 4), + ], + ), + if (isDesktopDeviceOrWeb && shouldShowReactions) ...[ + Padding( + padding: showUserAvatar != DisplayWidget.gone + ? EdgeInsets.only( + left: avatarWidth + 4, + right: avatarWidth + 4, + ) + : EdgeInsets.zero, + child: DesktopReactionsBuilder( + message: message, + messageTheme: messageTheme, + shouldShowReactions: shouldShowReactions, + borderSide: borderSide, + reverse: reverse, + ), + ), + ], + if (showBottomRow) + SizedBox( + height: context.textScaleFactor * 18.0, + ), + ], + ), + ), + if (isFailedState) + Positioned( + right: reverse ? 0 : null, + left: reverse ? null : 0, + bottom: showBottomRow ? 18 : -2, + child: StreamSvgIcon.error(size: 20), + ), + ], + ), + ], + ); + } + + void _showMessageReactionsModal(BuildContext context) { + final channel = StreamChannel.of(context).channel; + showDialog( + useRootNavigator: false, + context: context, + barrierColor: streamChatTheme.colorTheme.overlay, + builder: (context) => StreamChannel( + channel: channel, + child: StreamMessageReactionsModal( + messageWidget: messageWidget.copyWith( + key: const Key('MessageWidget'), + message: message.copyWith( + text: (message.text?.length ?? 0) > 200 + ? '${message.text!.substring(0, 200)}...' + : message.text, + ), + showReactions: false, + showUsername: false, + showTimestamp: false, + translateUserAvatar: false, + showSendingIndicator: false, + padding: EdgeInsets.zero, + showReactionPickerIndicator: + showReactions && (message.status == MessageSendingStatus.sent), + showPinHighlight: false, + showUserAvatar: + message.user!.id == channel.client.state.currentUser!.id + ? DisplayWidget.gone + : DisplayWidget.show, + ), + onUserAvatarTap: onUserAvatarTap, + messageTheme: messageTheme, + reverse: reverse, + message: message, + showReactions: showReactions, + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content_components.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content_components.dart new file mode 100644 index 00000000..ccbd8a0e --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content_components.dart @@ -0,0 +1,9 @@ +export 'bottom_row.dart'; +export 'message_card.dart'; +export 'parse_attachments.dart'; +export 'pinned_message.dart'; +export 'quoted_message.dart'; +export 'reactions/message_reactions_modal.dart'; +export 'reactions/reaction_bubble.dart'; +export 'reactions/reaction_indicator.dart'; +export 'user_avatar_transform.dart'; diff --git a/packages/stream_chat_flutter/lib/src/message_widget/parse_attachments.dart b/packages/stream_chat_flutter/lib/src/message_widget/parse_attachments.dart new file mode 100644 index 00000000..037c35f5 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/parse_attachments.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/message_widget/message_widget_content_components.dart'; +import 'package:stream_chat_flutter/src/utils/utils.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template parseAttachments} +/// Parses the attachments of a [StreamMessageWidget]. +/// +/// Used in [MessageCard]. Should not be used elsewhere. +/// {@endtemplate} +class ParseAttachments extends StatelessWidget { + /// {@macro parseAttachments} + const ParseAttachments({ + super.key, + required this.message, + required this.attachmentBuilders, + required this.attachmentPadding, + }); + + /// {@macro message} + final Message message; + + /// {@macro attachmentBuilders} + final Map attachmentBuilders; + + /// {@macro attachmentPadding} + final EdgeInsetsGeometry attachmentPadding; + + @override + Widget build(BuildContext context) { + final attachmentGroups = >{}; + + message.attachments + .where((element) => + (element.titleLink == null && element.type != null) || + element.type == 'giphy') + .forEach((e) { + if (attachmentGroups[e.type] == null) { + attachmentGroups[e.type!] = []; + } + + attachmentGroups[e.type]?.add(e); + }); + + final attachmentList = []; + + attachmentGroups.forEach((type, attachments) { + final attachmentBuilder = attachmentBuilders[type]; + + if (attachmentBuilder == null) return; + final attachmentWidget = attachmentBuilder( + context, + message, + attachments, + ); + attachmentList.add(attachmentWidget); + }); + + return Padding( + padding: attachmentPadding, + child: Column( + mainAxisSize: MainAxisSize.min, + children: attachmentList.insertBetween( + SizedBox( + height: attachmentPadding.vertical / 2, + ), + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/pinned_message.dart b/packages/stream_chat_flutter/lib/src/message_widget/pinned_message.dart new file mode 100644 index 00000000..55320a67 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/pinned_message.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template pinnedMessage} +/// A pinned message in a chat. +/// +/// Used in [MessageWidgetContent]. Should not be used elsewhere. +/// {@endtemplate} +class PinnedMessage extends StatelessWidget { + /// {@macro pinnedMessage} + const PinnedMessage({ + super.key, + required this.pinnedBy, + required this.currentUser, + }); + + /// The [User] who pinned this message. + final User pinnedBy; + + /// The current [User]. + final User currentUser; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + StreamSvgIcon.pin( + size: 16, + ), + const SizedBox( + width: 4, + ), + Text( + context.translations.pinnedByUserText( + pinnedBy: pinnedBy, + currentUser: currentUser, + ), + style: TextStyle( + color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, + fontSize: 13, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/quoted_message.dart b/packages/stream_chat_flutter/lib/src/message_widget/quoted_message.dart new file mode 100644 index 00000000..028cc443 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/quoted_message.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/message_input/quoted_message_widget.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template quotedMessage} +/// A quoted message in a chat. +/// +/// Used in [QuotedMessageCard]. Should not be used elsewhere. +/// {@endtemplate} +class QuotedMessage extends StatefulWidget { + /// {@macro quotedMessage} + const QuotedMessage({ + super.key, + required this.message, + required this.reverse, + required this.hasNonUrlAttachments, + this.onQuotedMessageTap, + }); + + /// {@macro message} + final Message message; + + /// {@macro onQuotedMessageTap} + final OnQuotedMessageTap? onQuotedMessageTap; + + /// {@macro reverse} + final bool reverse; + + /// {@macro hasNonUrlAttachments} + final bool hasNonUrlAttachments; + + @override + State createState() => _QuotedMessageState(); +} + +class _QuotedMessageState extends State { + late StreamChatState _streamChat; + late StreamChatThemeData _streamChatTheme; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _streamChatTheme = StreamChatTheme.of(context); + _streamChat = StreamChat.of(context); + } + + @override + Widget build(BuildContext context) { + final isMyMessage = widget.message.user?.id == _streamChat.currentUser?.id; + final onTap = widget.message.quotedMessage?.isDeleted != true && + widget.onQuotedMessageTap != null + ? () => widget.onQuotedMessageTap!(widget.message.quotedMessageId) + : null; + final chatThemeData = _streamChatTheme; + return StreamQuotedMessageWidget( + onTap: onTap, + message: widget.message.quotedMessage!, + messageTheme: isMyMessage + ? chatThemeData.otherMessageTheme + : chatThemeData.ownMessageTheme, + reverse: widget.reverse, + padding: EdgeInsets.only( + right: 8, + left: 8, + top: 8, + bottom: widget.hasNonUrlAttachments ? 8 : 0, + ), + composing: false, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/reactions/desktop_reactions_builder.dart b/packages/stream_chat_flutter/lib/src/message_widget/reactions/desktop_reactions_builder.dart new file mode 100644 index 00000000..8ddcb917 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/reactions/desktop_reactions_builder.dart @@ -0,0 +1,367 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_portal/flutter_portal.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +// ignore_for_file: cascade_invocations + +/// {@template desktopReactionsBuilder} +/// Builds a list of reactions to a message on desktop & web. +/// +/// Not intended for use outside of [MessageWidgetContent]. +/// {@endtemplate} +class DesktopReactionsBuilder extends StatefulWidget { + /// {@macro desktopReactionsBuilder} + const DesktopReactionsBuilder({ + super.key, + required this.shouldShowReactions, + required this.message, + required this.messageTheme, + this.borderSide, + required this.reverse, + }); + + /// Whether reactions should be shown. + final bool shouldShowReactions; + + /// The message to show reactions for. + final Message message; + + /// The theme to use for the reactions. + /// + /// [StreamMessageThemeData] is used because the design spec for desktop + /// reactions matches the design spec for messages. + final StreamMessageThemeData messageTheme; + + /// {@macro borderSide} + final BorderSide? borderSide; + + /// {@macro reverse} + final bool reverse; + + @override + State createState() => + _DesktopReactionsBuilderState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add( + DiagnosticsProperty( + 'shouldShowReactions', + shouldShowReactions, + ), + ); + properties.add( + DiagnosticsProperty('message', message), + ); + properties.add( + DiagnosticsProperty( + 'messageTheme', + messageTheme, + ), + ); + properties.add( + DiagnosticsProperty('borderSide', borderSide), + ); + properties.add(DiagnosticsProperty('reverse', reverse)); + } +} + +class _DesktopReactionsBuilderState extends State { + bool _showReactionsPopup = false; + + @override + Widget build(BuildContext context) { + final streamChat = StreamChat.of(context); + final reactionIcons = StreamChatConfiguration.of(context).reactionIcons; + final streamChatTheme = StreamChatTheme.of(context); + + final reactionsMap = {}; + var reactionsList = []; + if (widget.shouldShowReactions) { + widget.message.latestReactions?.forEach((element) { + if (!reactionsMap.containsKey(element.type) || + element.user!.id == streamChat.currentUser?.id) { + reactionsMap[element.type] = element; + } + }); + + reactionsList = reactionsMap.values.toList() + ..sort((a, b) => a.user!.id == streamChat.currentUser?.id ? 1 : -1); + } + + return PortalTarget( + visible: _showReactionsPopup, + portalCandidateLabels: const [kPortalMessageListViewLabel], + anchor: Aligned( + target: widget.reverse ? Alignment.topRight : Alignment.topLeft, + follower: widget.reverse ? Alignment.bottomRight : Alignment.bottomLeft, + shiftToWithinBound: const AxisFlag( + y: true, + ), + ), + portalFollower: MouseRegion( + onEnter: (event) async { + setState(() => _showReactionsPopup = !_showReactionsPopup); + }, + onExit: (event) { + setState(() => _showReactionsPopup = !_showReactionsPopup); + }, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 336, + maxHeight: 342, + ), + child: Card( + color: streamChatTheme.colorTheme.barsBg, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text( + '''${widget.message.latestReactions!.length} ${context.translations.messageReactionsLabel}''', + style: streamChatTheme.textTheme.headlineBold, + ), + ), + Flexible( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Wrap( + spacing: 16, + runSpacing: 16, + children: [ + ...widget.message.latestReactions!.map((reaction) { + final reactionIcon = reactionIcons.firstWhereOrNull( + (r) => r.type == reaction.type, + ); + return _StackedReaction( + reaction: reaction, + streamChatTheme: streamChatTheme, + reactionIcon: reactionIcon, + ); + }).toList(), + ], + ), + ), + ), + ], + ), + ), + ), + ), + child: MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (event) async { + setState(() => _showReactionsPopup = !_showReactionsPopup); + }, + onExit: (event) { + setState(() => _showReactionsPopup = !_showReactionsPopup); + }, + child: Wrap( + children: [ + ...reactionsList.map((reaction) { + final reactionIcon = reactionIcons.firstWhereOrNull( + (r) => r.type == reaction.type, + ); + + return _BottomReaction( + reaction: reaction, + message: widget.message, + borderSide: widget.borderSide, + messageTheme: widget.messageTheme, + reactionIcon: reactionIcon, + streamChatTheme: streamChatTheme, + ); + }).toList(), + ], + ), + ), + ); + } +} + +class _BottomReaction extends StatelessWidget { + const _BottomReaction({ + required this.reaction, + required this.message, + required this.borderSide, + required this.messageTheme, + required this.reactionIcon, + required this.streamChatTheme, + }); + + final Reaction reaction; + final Message message; + final BorderSide? borderSide; + final StreamMessageThemeData? messageTheme; + final StreamReactionIcon? reactionIcon; + final StreamChatThemeData streamChatTheme; + + @override + Widget build(BuildContext context) { + final userId = StreamChat.of(context).currentUser?.id; + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (reaction.userId == userId) { + StreamChannel.of(context).channel.deleteReaction( + message, + reaction, + ); + } else if (reactionIcon != null) { + StreamChannel.of(context).channel.sendReaction( + message, + reactionIcon!.type, + score: reaction.score + 1, + enforceUnique: + StreamChatConfiguration.of(context).enforceUniqueReactions, + ); + } + }, + child: Card( + shape: StadiumBorder( + side: borderSide ?? + BorderSide( + color: messageTheme?.messageBorderColor ?? Colors.grey, + ), + ), + color: messageTheme?.messageBackgroundColor, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + ConstrainedBox( + constraints: BoxConstraints.tight( + const Size.square(16), + ), + child: reactionIcon?.builder( + context, + reaction.user?.id == userId, + 16, + ) ?? + Icon( + Icons.help_outline_rounded, + size: 16, + color: reaction.user?.id == userId + ? streamChatTheme.colorTheme.accentPrimary + : streamChatTheme.colorTheme.textHighEmphasis + .withOpacity(0.5), + ), + ), + const SizedBox(width: 4), + Text( + '${reaction.score}', + style: const TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('reaction', reaction)); + properties.add(DiagnosticsProperty('message', message)); + } +} + +class _StackedReaction extends StatelessWidget { + const _StackedReaction({ + required this.reaction, + required this.streamChatTheme, + required this.reactionIcon, + }); + + final Reaction reaction; + final StreamChatThemeData streamChatTheme; + final StreamReactionIcon? reactionIcon; + + @override + Widget build(BuildContext context) { + final userId = StreamChat.of(context).currentUser?.id; + return SizedBox( + width: 80, + child: Column( + children: [ + Stack( + children: [ + StreamUserAvatar( + user: reaction.user!, + constraints: const BoxConstraints.tightFor( + height: 64, + width: 64, + ), + borderRadius: BorderRadius.circular(32), + ), + Positioned( + bottom: 0, + right: 0, + child: DecoratedBox( + decoration: BoxDecoration( + color: streamChatTheme.colorTheme.inputBg, + border: Border.all( + color: streamChatTheme.colorTheme.barsBg, + width: 2, + ), + shape: BoxShape.circle, + ), + child: Padding( + padding: const EdgeInsets.all(8), + child: reactionIcon?.builder( + context, + reaction.userId == userId, + 16, + ) ?? + Icon( + Icons.help_outline_rounded, + size: 16, + color: reaction.user?.id == userId + ? streamChatTheme.colorTheme.accentPrimary + : streamChatTheme.colorTheme.textHighEmphasis + .withOpacity(0.5), + ), + ), + ), + ), + ], + ), + Text( + userId == reaction.user!.name ? 'You' : reaction.user!.name, + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add( + DiagnosticsProperty('reaction', reaction), + ); + properties.add( + DiagnosticsProperty( + 'reactionIcon', + reactionIcon, + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart similarity index 91% rename from packages/stream_chat_flutter/lib/src/message_reactions_modal.dart rename to packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart index e4317b01..19608d78 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart @@ -1,19 +1,15 @@ import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/reaction_bubble.dart'; +import 'package:stream_chat_flutter/src/message_widget/reactions/reaction_bubble.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@macro message_reactions_modal} -@Deprecated("Use 'StreamMessageReactionsModal' instead") -typedef MessageReactionsModal = StreamMessageReactionsModal; - -/// {@template message_reactions_modal} +/// {@template streamMessageReactionsModal} /// Modal widget for displaying message reactions /// {@endtemplate} class StreamMessageReactionsModal extends StatelessWidget { - /// Constructor for creating a [StreamMessageReactionsModal] reactions + /// {@macro streamMessageReactionsModal} const StreamMessageReactionsModal({ super.key, required this.message, @@ -33,13 +29,13 @@ class StreamMessageReactionsModal extends StatelessWidget { /// [StreamMessageThemeData] to apply to [message] final StreamMessageThemeData messageTheme; - /// Flag to reverse message + /// {@macro reverse} final bool reverse; - /// Flag to show reactions on message + /// {@macro showReactions} final bool? showReactions; - /// Callback when user avatar is tapped + /// {@macro onUserAvatarTap} final void Function(User)? onUserAvatarTap; @override @@ -69,7 +65,8 @@ class StreamMessageReactionsModal extends StatelessWidget { ? 1 : (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize)); - final numberOfReactions = StreamChatTheme.of(context).reactionIcons.length; + final numberOfReactions = + StreamChatConfiguration.of(context).reactionIcons.length; final shiftFactor = numberOfReactions < 5 ? (5 - numberOfReactions) * 0.1 : 0.0; @@ -117,7 +114,7 @@ class StreamMessageReactionsModal extends StatelessWidget { return GestureDetector( behavior: HitTestBehavior.translucent, - onTap: () => Navigator.maybePop(context), + onTap: () => Navigator.of(context).maybePop(), child: Stack( children: [ Positioned.fill( @@ -196,10 +193,9 @@ class StreamMessageReactionsModal extends StatelessWidget { final isCurrentUser = reaction.user?.id == currentUser.id; final chatThemeData = StreamChatTheme.of(context); return ConstrainedBox( - constraints: BoxConstraints.loose(const Size( - 64, - 98, - )), + constraints: BoxConstraints.loose( + const Size(64, 100), + ), child: Column( mainAxisSize: MainAxisSize.min, children: [ diff --git a/packages/stream_chat_flutter/lib/src/reaction_bubble.dart b/packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_bubble.dart similarity index 96% rename from packages/stream_chat_flutter/lib/src/reaction_bubble.dart rename to packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_bubble.dart index 2944482c..c3c3c762 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_bubble.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_bubble.dart @@ -4,15 +4,11 @@ import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@macro reaction_bubble} -@Deprecated("Use 'StreamReactionBubble' instead") -typedef ReactionBubble = StreamReactionBubble; - -/// {@template reaction_bubble} -/// Creates reaction bubble widget for displaying over messages +/// {@template streamReactionBubble} +/// Creates a reaction bubble that displays over messages. /// {@endtemplate} class StreamReactionBubble extends StatelessWidget { - /// Constructor for creating a [StreamReactionBubble] + /// {@macro streamReactionBubble} const StreamReactionBubble({ super.key, required this.reactions, @@ -51,7 +47,7 @@ class StreamReactionBubble extends StatelessWidget { @override Widget build(BuildContext context) { - final reactionIcons = StreamChatTheme.of(context).reactionIcons; + final reactionIcons = StreamChatConfiguration.of(context).reactionIcons; final totalReactions = reactions.length; final offset = totalReactions > 1 ? 16.0.mirrorConditionally(flipTail) : 2.0; diff --git a/packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_indicator.dart b/packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_indicator.dart new file mode 100644 index 00000000..87bae568 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_indicator.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/message_widget/reactions/reaction_bubble.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template reactionIndicator} +/// Indicates the reaction a [StreamMessageWidget] has. +/// +/// Used in [MessageWidgetContent]. +/// {@endtemplate} +class ReactionIndicator extends StatelessWidget { + /// {@macro reactionIndicator} + const ReactionIndicator({ + super.key, + required this.ownId, + required this.message, + required this.shouldShowReactions, + required this.onTap, + required this.reverse, + required this.messageTheme, + }); + + /// The id of the current user. + final String ownId; + + /// {@macro message} + final Message message; + + /// {@macro shouldShowReactions} + final bool shouldShowReactions; + + /// The callback to perform when the widget is tapped or clicked. + final VoidCallback onTap; + + /// {@macro reverse} + final bool reverse; + + /// {@macro messageTheme} + final StreamMessageThemeData messageTheme; + + @override + Widget build(BuildContext context) { + final reactionsMap = {}; + message.latestReactions?.forEach((element) { + if (!reactionsMap.containsKey(element.type) || + element.user!.id == ownId) { + reactionsMap[element.type] = element; + } + }); + final reactionsList = reactionsMap.values.toList() + ..sort((a, b) => a.user!.id == ownId ? 1 : -1); + + return Transform( + transform: Matrix4.translationValues( + reverse ? 12 : -12, + 0, + 0, + ), + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 22 * 6.0, + ), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: shouldShowReactions + ? GestureDetector( + onTap: onTap, + child: StreamReactionBubble( + key: ValueKey('${message.id}.reactions'), + reverse: reverse, + flipTail: reverse, + backgroundColor: messageTheme.reactionsBackgroundColor ?? + Colors.transparent, + borderColor: + messageTheme.reactionsBorderColor ?? Colors.transparent, + maskColor: + messageTheme.reactionsMaskColor ?? Colors.transparent, + reactions: reactionsList, + ), + ) + : const SizedBox(), + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/reaction_picker.dart b/packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_picker.dart similarity index 87% rename from packages/stream_chat_flutter/lib/src/reaction_picker.dart rename to packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_picker.dart index a6eaab39..6017f8cb 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_picker.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_picker.dart @@ -1,23 +1,19 @@ import 'package:ezanimation/ezanimation.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@macro reaction_picker} -@Deprecated("Use 'StreamReactionPicker' instead") -typedef ReactionPicker = StreamReactionPicker; - -/// {@template reaction_picker} +/// {@template streamReactionPicker} /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/reaction_picker.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/reaction_picker_paint.png) /// -/// It shows a reaction picker +/// Allows the user to select reactions to a message on mobile. /// -/// Usually you don't use this widget as it's one of the default widgets used -/// by [StreamMessageWidget.onMessageActions]. +/// It is not recommended to use this widget directly as it's one of the +/// default widgets used by [StreamMessageWidget.onMessageActions]. /// {@endtemplate} class StreamReactionPicker extends StatefulWidget { - /// Constructor for creating a [StreamReactionPicker] widget + /// {@macro streamReactionPicker} const StreamReactionPicker({ super.key, required this.message, @@ -37,7 +33,7 @@ class _StreamReactionPickerState extends State @override Widget build(BuildContext context) { final chatThemeData = StreamChatTheme.of(context); - final reactionIcons = chatThemeData.reactionIcons; + final reactionIcons = StreamChatConfiguration.of(context).reactionIcons; if (animations.isEmpty && reactionIcons.isNotEmpty) { reactionIcons.forEach((element) { @@ -119,9 +115,11 @@ class _StreamReactionPickerState extends State ), ); }) - .insertBetween(const SizedBox( - width: 16, - )) + .insertBetween( + const SizedBox( + width: 16, + ), + ) .toList(), ), ), @@ -139,14 +137,14 @@ class _StreamReactionPickerState extends State ); } - void triggerAnimations() async { + Future triggerAnimations() async { for (final a in animations) { a.start(); await Future.delayed(const Duration(milliseconds: 100)); } } - void pop() async { + Future pop() async { for (final a in animations) { a.stop(); } @@ -158,7 +156,8 @@ class _StreamReactionPickerState extends State StreamChannel.of(context).channel.sendReaction( widget.message, reactionType, - enforceUnique: true, + enforceUnique: + StreamChatConfiguration.of(context).enforceUniqueReactions, ); pop(); } diff --git a/packages/stream_chat_flutter/lib/src/message_widget/sending_indicator_wrapper.dart b/packages/stream_chat_flutter/lib/src/message_widget/sending_indicator_wrapper.dart new file mode 100644 index 00000000..58bfb49a --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/sending_indicator_wrapper.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template sendingIndicatorWrapper} +/// Helper widget for building a [StreamSendingIndicator]. +/// +/// Used in [BottomRow]. Should not be used elsewhere. +/// {@endtemplate} +class SendingIndicatorWrapper extends StatelessWidget { + /// {@macro sendingIndicatorWrapper} + const SendingIndicatorWrapper({ + super.key, + required this.messageTheme, + required this.message, + required this.hasNonUrlAttachments, + required this.streamChat, + required this.streamChatTheme, + }); + + /// {@macro messageTheme} + final StreamMessageThemeData messageTheme; + + /// {@macro message} + final Message message; + + /// {@macro hasNonUrlAttachments} + final bool hasNonUrlAttachments; + + /// {@macro streamChat} + final StreamChatState streamChat; + + /// {@macro streamChatThemeData} + final StreamChatThemeData streamChatTheme; + + @override + Widget build(BuildContext context) { + final style = messageTheme.createdAtStyle; + final memberCount = StreamChannel.of(context).channel.memberCount ?? 0; + + if (hasNonUrlAttachments && + (message.status == MessageSendingStatus.sending || + message.status == MessageSendingStatus.updating)) { + final totalAttachments = message.attachments.length; + final uploadRemaining = + message.attachments.where((it) => !it.uploadState.isSuccess).length; + if (uploadRemaining == 0) { + return StreamSvgIcon.check( + size: style!.fontSize, + color: IconTheme.of(context).color!.withOpacity(0.5), + ); + } + return Text( + context.translations.attachmentsUploadProgressText( + remaining: uploadRemaining, + total: totalAttachments, + ), + style: style, + ); + } + + final channel = StreamChannel.of(context).channel; + + return BetterStreamBuilder>( + stream: channel.state?.readStream, + initialData: channel.state?.read, + builder: (context, data) { + final readList = data.where((it) => + it.user.id != streamChat.currentUser?.id && + (it.lastRead.isAfter(message.createdAt) || + it.lastRead.isAtSameMomentAs(message.createdAt))); + final isMessageRead = readList.length >= (channel.memberCount ?? 0) - 1; + Widget child = StreamSendingIndicator( + message: message, + isMessageRead: isMessageRead, + size: style!.fontSize, + ); + if (isMessageRead) { + child = Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (memberCount > 2) + Text( + readList.length.toString(), + style: style.copyWith( + color: streamChatTheme.colorTheme.accentPrimary, + ), + ), + const SizedBox(width: 2), + child, + ], + ); + } + return child; + }, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/text_bubble.dart b/packages/stream_chat_flutter/lib/src/message_widget/text_bubble.dart new file mode 100644 index 00000000..2fa017ed --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/text_bubble.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/message_widget/message_widget_content_components.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template textBubble} +/// The bubble around a [StreamMessageText]. +/// +/// Used in [MessageCard]. Should not be used elsewhere. +/// {@endtemplate} +class TextBubble extends StatelessWidget { + /// {@macro textBubble} + const TextBubble({ + super.key, + required this.message, + required this.isOnlyEmoji, + required this.textPadding, + required this.messageTheme, + required this.hasUrlAttachments, + required this.hasQuotedMessage, + this.textBuilder, + this.onLinkTap, + this.onMentionTap, + }); + + /// {@macro message} + final Message message; + + /// {@macro isOnlyEmoji} + final bool isOnlyEmoji; + + /// {@macro textPadding} + final EdgeInsets textPadding; + + /// {@macro textBuilder} + final Widget Function(BuildContext, Message)? textBuilder; + + /// {@macro onLinkTap} + final void Function(String)? onLinkTap; + + /// {@macro onMentionTap} + final void Function(User)? onMentionTap; + + /// {@macro messageTheme} + final StreamMessageThemeData messageTheme; + + /// {@macro hasUrlAttachments} + final bool hasUrlAttachments; + + /// {@macro hasQuotedMessage} + final bool hasQuotedMessage; + + @override + Widget build(BuildContext context) { + if (message.text?.trim().isEmpty ?? false) return const Offstage(); + return Padding( + padding: isOnlyEmoji ? EdgeInsets.zero : textPadding, + child: textBuilder != null + ? textBuilder!(context, message) + : StreamMessageText( + onLinkTap: onLinkTap, + message: message, + onMentionTap: onMentionTap, + messageTheme: isOnlyEmoji + ? messageTheme.copyWith( + messageTextStyle: messageTheme.messageTextStyle!.copyWith( + fontSize: 42, + ), + ) + : messageTheme, + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/thread_painter.dart b/packages/stream_chat_flutter/lib/src/message_widget/thread_painter.dart new file mode 100644 index 00000000..09bb63c9 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/thread_painter.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template threadReplyPainter} +/// A custom painter used to render thread replies. +/// +/// Used in [BottomRow]. +/// {@endtemplate} +class ThreadReplyPainter extends CustomPainter { + /// {@macro threadReplyPainter} + const ThreadReplyPainter({ + this.context, + required this.color, + this.reverse = false, + }); + + /// The color to paint the thread reply with. + final Color? color; + + /// The [BuildContext] to use to retrieve the [StreamChatTheme]. + final BuildContext? context; + + /// {@macro reverse} + final bool reverse; + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color ?? StreamChatTheme.of(context!).colorTheme.disabled + ..style = PaintingStyle.stroke + ..strokeWidth = 1 + ..strokeCap = StrokeCap.round; + + final path = Path() + ..moveTo(reverse ? size.width : 0, 0) + ..quadraticBezierTo( + reverse ? size.width : 0, + size.height * 0.38, + reverse ? size.width : 0, + size.height * 0.5, + ) + ..quadraticBezierTo( + reverse ? size.width : 0, + size.height, + reverse ? 0 : size.width, + size.height, + ); + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/thread_participants.dart b/packages/stream_chat_flutter/lib/src/message_widget/thread_participants.dart new file mode 100644 index 00000000..5068e293 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/thread_participants.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template threadParticipants} +/// Shows the users participating in a thread. +/// +/// Used in [BottomRow]. +/// {@endtemplate} +class ThreadParticipants extends StatelessWidget { + /// {@macro threadParticipants} + const ThreadParticipants({ + super.key, + required StreamChatThemeData streamChatTheme, + required this.threadParticipants, + }) : _streamChatTheme = streamChatTheme; + + /// {@macro streamChatThemeData} + final StreamChatThemeData _streamChatTheme; + + /// The users participating in the thread. + final Iterable threadParticipants; + + @override + Widget build(BuildContext context) { + var padding = 0.0; + return Stack( + children: threadParticipants.map((user) { + padding += 8.0; + return Positioned( + right: padding - 8, + bottom: 0, + top: 0, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: _streamChatTheme.colorTheme.barsBg, + ), + padding: const EdgeInsets.all(1), + child: StreamUserAvatar( + user: user, + constraints: BoxConstraints.tight(const Size.fromRadius(7)), + showOnlineStatus: false, + ), + ), + ); + }).toList(), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/user_avatar_transform.dart b/packages/stream_chat_flutter/lib/src/message_widget/user_avatar_transform.dart new file mode 100644 index 00000000..275d63e0 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/user_avatar_transform.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template userAvatarTransform} +/// Transforms a [StreamUserAvatar] according to the specified translation. +/// +/// Used in [MessageWidgetContent]. +/// {@endtemplate} +class UserAvatarTransform extends StatelessWidget { + /// {@macro userAvatarTransform} + const UserAvatarTransform({ + super.key, + required this.translateUserAvatar, + required this.messageTheme, + required this.message, + this.userAvatarBuilder, + this.onUserAvatarTap, + }); + + /// {@macro translateUserAvatar} + final bool translateUserAvatar; + + /// {@macro messageTheme} + final StreamMessageThemeData messageTheme; + + /// {@macro userAvatarBuilder} + final Widget Function(BuildContext, User)? userAvatarBuilder; + + /// {@macro message} + final Message message; + + /// {@macro onUserAvatarTap} + final void Function(User)? onUserAvatarTap; + + @override + Widget build(BuildContext context) { + return Transform.translate( + offset: Offset( + 0, + translateUserAvatar + ? (messageTheme.avatarTheme?.constraints.maxHeight ?? 40) / 2 + : 0, + ), + child: userAvatarBuilder?.call(context, message.user!) ?? + StreamUserAvatar( + user: message.user!, + onTap: onUserAvatarTap, + constraints: messageTheme.avatarTheme!.constraints, + borderRadius: messageTheme.avatarTheme!.borderRadius, + showOnlineStatus: false, + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/username.dart b/packages/stream_chat_flutter/lib/src/message_widget/username.dart new file mode 100644 index 00000000..fad0ff6f --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/username.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template username} +/// Displays the username of a particular message's sender. +/// {@endtemplate} +class Username extends StatelessWidget { + /// {@macro username} + const Username({ + super.key, + required this.message, + required this.messageTheme, + }); + + /// {@macro message} + final Message message; + + /// {@macro messageTheme} + final StreamMessageThemeData messageTheme; + + @override + Widget build(BuildContext context) { + return Text( + message.user?.name ?? '', + maxLines: 1, + key: key, + style: messageTheme.messageAuthorStyle, + overflow: TextOverflow.ellipsis, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/misc/back_button.dart b/packages/stream_chat_flutter/lib/src/misc/back_button.dart new file mode 100644 index 00000000..22245e01 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/misc/back_button.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template streamBackButton} +/// A custom back button implementation +/// {@endtemplate} +// ignore: prefer-match-file-name +class StreamBackButton extends StatelessWidget { + /// {@macro streamBackButton} + const StreamBackButton({ + super.key, + this.onPressed, + this.showUnreadCount = false, + this.channelId, + }); + + /// Callback for when button is pressed + final VoidCallback? onPressed; + + /// Show unread count + final bool showUnreadCount; + + /// Channel ID used to retrieve unread count + final String? channelId; + + @override + Widget build(BuildContext context) { + return Stack( + alignment: Alignment.center, + children: [ + RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + elevation: 0, + highlightElevation: 0, + focusElevation: 0, + hoverElevation: 0, + onPressed: () { + if (onPressed != null) { + onPressed!(); + } else { + Navigator.of(context).maybePop(); + } + }, + padding: const EdgeInsets.all(14), + child: StreamSvgIcon.left( + size: 24, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, + ), + ), + if (showUnreadCount) + Positioned( + top: 7, + right: 7, + child: StreamUnreadIndicator( + cid: channelId, + ), + ), + ], + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/connection_status_builder.dart b/packages/stream_chat_flutter/lib/src/misc/connection_status_builder.dart similarity index 83% rename from packages/stream_chat_flutter/lib/src/connection_status_builder.dart rename to packages/stream_chat_flutter/lib/src/misc/connection_status_builder.dart index dd47a626..161112ec 100644 --- a/packages/stream_chat_flutter/lib/src/connection_status_builder.dart +++ b/packages/stream_chat_flutter/lib/src/misc/connection_status_builder.dart @@ -1,19 +1,15 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@macro connection_status_builder} -@Deprecated("Use 'StreamConnectionStatusBuilder' instead") -typedef ConnectionStatusBuilder = StreamConnectionStatusBuilder; - -/// {@template connection_status_builder} -/// Widget that builds itself based on the latest snapshot of interaction with +/// {@template streamConnectionStatusBuilder} +/// A widget that builds itself based on the latest snapshot of interaction with /// a [Stream] of type [ConnectionStatus]. /// /// The widget will use the closest [StreamChatClient.wsConnectionStatusStream] /// in case no stream is provided. /// {@endtemplate} class StreamConnectionStatusBuilder extends StatelessWidget { - /// Creates a new ConnectionStatusBuilder + /// {@macro streamConnectionStatusBuilder} const StreamConnectionStatusBuilder({ super.key, required this.statusBuilder, diff --git a/packages/stream_chat_flutter/lib/src/date_divider.dart b/packages/stream_chat_flutter/lib/src/misc/date_divider.dart similarity index 80% rename from packages/stream_chat_flutter/lib/src/date_divider.dart rename to packages/stream_chat_flutter/lib/src/misc/date_divider.dart index 6cdb034d..751f5dbd 100644 --- a/packages/stream_chat_flutter/lib/src/date_divider.dart +++ b/packages/stream_chat_flutter/lib/src/misc/date_divider.dart @@ -1,17 +1,13 @@ import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; -/// {@macro date_divider} -@Deprecated("Use 'StreamDateDivider' instead") -typedef DateDivider = StreamDateDivider; - -/// {@template date_divider} -/// It shows a date divider depending on the date difference +/// {@template streamDateDivider} +/// Shows a date divider depending on the date difference /// {@endtemplate} class StreamDateDivider extends StatelessWidget { - /// Constructor for creating a [StreamDateDivider] + /// {@macro streamDateDivider} const StreamDateDivider({ super.key, required this.dateTime, diff --git a/packages/stream_chat_flutter/lib/src/info_tile.dart b/packages/stream_chat_flutter/lib/src/misc/info_tile.dart similarity index 83% rename from packages/stream_chat_flutter/lib/src/info_tile.dart rename to packages/stream_chat_flutter/lib/src/misc/info_tile.dart index a100f7e3..75bc6d4f 100644 --- a/packages/stream_chat_flutter/lib/src/info_tile.dart +++ b/packages/stream_chat_flutter/lib/src/misc/info_tile.dart @@ -1,16 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_portal/flutter_portal.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; -/// {@macro info_tile} -@Deprecated("Use 'StreamInfoTile' instead") -typedef InfoTile = StreamInfoTile; - -/// {@template info_tile} -/// Tile to display a message, used in stream chat to display connection status +/// {@template streamInfoTile} +/// Displays a message. Often used to display connection status. /// {@endtemplate} class StreamInfoTile extends StatelessWidget { - /// Constructor for creating an [StreamInfoTile] widget + /// {@macro streamInfoTile} const StreamInfoTile({ super.key, required this.message, diff --git a/packages/stream_chat_flutter/lib/src/option_list_tile.dart b/packages/stream_chat_flutter/lib/src/misc/option_list_tile.dart similarity index 88% rename from packages/stream_chat_flutter/lib/src/option_list_tile.dart rename to packages/stream_chat_flutter/lib/src/misc/option_list_tile.dart index c7e8bec0..ddb1db76 100644 --- a/packages/stream_chat_flutter/lib/src/option_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/misc/option_list_tile.dart @@ -1,15 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; -/// {@macro option_list_tile} -@Deprecated("Use 'StreamOptionListTile' instead") -typedef OptionListTile = StreamOptionListTile; - -/// {@template option_list_tile} +/// {@template streamOptionListTile} /// List tile for [ChannelBottomSheet] /// {@endtemplate} class StreamOptionListTile extends StatelessWidget { - /// Constructor for creating [StreamOptionListTile] + /// {@macro streamOptionListTile} const StreamOptionListTile({ super.key, required this.title, @@ -31,7 +27,7 @@ class StreamOptionListTile extends StatelessWidget { /// Trailing widget (end) final Widget? trailing; - /// Callback when tile is tapped + /// The action to perform when the tile is tapped final VoidCallback? onTap; /// Title color diff --git a/packages/stream_chat_flutter/lib/src/reaction_icon.dart b/packages/stream_chat_flutter/lib/src/misc/reaction_icon.dart similarity index 63% rename from packages/stream_chat_flutter/lib/src/reaction_icon.dart rename to packages/stream_chat_flutter/lib/src/misc/reaction_icon.dart index cf2cef81..ebccc153 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_icon.dart +++ b/packages/stream_chat_flutter/lib/src/misc/reaction_icon.dart @@ -1,13 +1,11 @@ import 'package:flutter/material.dart'; +/// {@template streamReactionIcon} /// Reaction icon data -@Deprecated("Use 'StreamReactionIcon' instead") -typedef ReactionIcon = StreamReactionIcon; - -/// Reaction icon data +/// {@endtemplate} class StreamReactionIcon { - /// Constructor for creating [StreamReactionIcon] - StreamReactionIcon({ + /// {@macro streamReactionIcon} + const StreamReactionIcon({ required this.type, required this.builder, }); diff --git a/packages/stream_chat_flutter/lib/src/misc/stream_neumorphic_button.dart b/packages/stream_chat_flutter/lib/src/misc/stream_neumorphic_button.dart new file mode 100644 index 00000000..0c6ca158 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/misc/stream_neumorphic_button.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +/// {@template neumorphicButton} +/// Neumorphic button +/// {@endtemplate} +class StreamNeumorphicButton extends StatelessWidget { + /// {@macro neumorphicButton} + const StreamNeumorphicButton({ + super.key, + required this.child, + this.backgroundColor = Colors.white, + }); + + /// Child contained in the button + final Widget child; + + /// Background color of the button + final Color backgroundColor; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.all(8), + height: 40, + width: 40, + decoration: BoxDecoration( + color: backgroundColor, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.grey.shade700, + offset: const Offset(0, 1), + blurRadius: 0.5, + ), + const BoxShadow( + color: Colors.white, + blurRadius: 0.5, + ), + ], + ), + child: child, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/misc/stream_svg_icon.dart b/packages/stream_chat_flutter/lib/src/misc/stream_svg_icon.dart new file mode 100644 index 00000000..8472af6e --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/misc/stream_svg_icon.dart @@ -0,0 +1,1146 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +/// {@template streamSvgIcon} +/// Icon set of stream chat +/// {@endtemplate} +class StreamSvgIcon extends StatelessWidget { + /// {@macro streamSvgIcon} + const StreamSvgIcon({ + super.key, + this.assetName, + this.color, + this.width, + this.height, + }); + + /// [StreamSvgIcon] type + factory StreamSvgIcon.settings({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'settings.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.down({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_down.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.up({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_up.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.attach({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_attach.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.loveReaction({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_love_reaction.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.thumbsUpReaction({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_thumbs_up_reaction.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.thumbsDownReaction({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_thumbs_down_reaction.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.lolReaction({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_LOL_reaction.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.wutReaction({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_wut_reaction.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.smile({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_smile.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.mentions({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'mentions.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.record({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_record.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.camera({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_camera.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.files({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'files.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.pictures({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'pictures.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.left({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_left.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.user({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_user.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.userAdd({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_User_add.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.check({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_check.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.checkAll({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_check_all.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.checkSend({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_check_send.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.penWrite({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_pen-write.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.contacts({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_contacts.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.close({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_close.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.search({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_search.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.right({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_right.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.mute({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_mute.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.userRemove({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_User_deselect.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.lightning({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_lightning-command runner.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.emptyCircleLeft({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_empty_circle_left.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.message({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_message.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.thread({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_Thread_Reply.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.reply({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_curve_line_left_up_big.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.edit({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_edit.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.download({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_download.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.cloudDownload({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_cloud_download.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.copy({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_copy.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.delete({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_delete.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.eye({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_eye-off.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.arrowRight({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_arrow_right.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.closeSmall({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_close_sml.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.iconCurveLineLeftUp({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_curve_line_left_up.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.iconMoon({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'icon_moon.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.iconShare({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'icon_SHARE.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.iconGrid({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_grid.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.iconSendMessage({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_send_message.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.iconMenuPoint({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_menu_point_v.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.iconSave({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_save.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.shareArrow({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'share_arrow.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.filetype7z({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'filetype_7z.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.filetypeCsv({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'filetype_CSV.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.filetypeDoc({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'filetype_DOC.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.filetypeDocx({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'filetype_DOCX.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.filetypeGeneric({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'filetype_Generic.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.filetypeHtml({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'filetype_html.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.filetypeMd({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'filetype_MD.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.filetypeOdt({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'filetype_ODT.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.filetypePdf({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'filetype_PDF.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.filetypePpt({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'filetype_PPT.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.filetypePptx({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'filetype_PPTX.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.filetypeRar({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'filetype_RAR.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.filetypeRtf({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'filetype_RTF.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.filetypeTar({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'filetype_TAR.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.filetypeTxt({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'filetype_TXT.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.filetypeXls({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'filetype_XLS.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.filetypeXlsx({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'filetype_XLSX.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.filetypeZip({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'filetype_ZIP.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.iconGroup({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_group.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.iconNotification({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_notification.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.iconUserDelete({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_user_delete.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.error({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_error.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.circleUp({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_circle_up.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.iconUserSettings({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'Icon_user_settings.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.giphyIcon({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'giphy_icon.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.imgur({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'imgur.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.volumeUp({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'volume-up.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.flag({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'flag.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.iconFlag({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'icon_flag.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.retry({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'icon_retry.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.pin({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'icon_pin.svg', + color: color, + width: size, + height: size, + ); + } + + /// [StreamSvgIcon] type + factory StreamSvgIcon.videoCall({ + double? size, + Color? color, + }) { + return StreamSvgIcon( + assetName: 'video_call_icon.svg', + color: color, + width: size, + height: size, + ); + } + + /// Name of icon asset + final String? assetName; + + /// Width of icon + final double? width; + + /// Height of icon + final double? height; + + /// Color of icon + final Color? color; + + @override + Widget build(BuildContext context) { + final key = Key('StreamSvgIcon-$assetName'); + return SvgPicture.asset( + 'lib/svgs/$assetName', + package: 'stream_chat_flutter', + key: key, + width: width, + height: height, + color: color, + ); + } +} + +/// Alternative of [StreamSvgIcon] which follows the [IconTheme]. +class StreamIconThemeSvgIcon extends StatelessWidget { + /// Creates a [StreamIconThemeSvgIcon]. + const StreamIconThemeSvgIcon({ + super.key, + this.assetName, + this.width, + this.height, + this.color, + }); + + /// Factory constructor to create [StreamIconThemeSvgIcon] + /// from [StreamSvgIcon]. + factory StreamIconThemeSvgIcon.fromStreamSvgIcon( + StreamSvgIcon streamSvgIcon, + ) { + return StreamIconThemeSvgIcon( + assetName: streamSvgIcon.assetName, + width: streamSvgIcon.width, + height: streamSvgIcon.height, + color: streamSvgIcon.color, + ); + } + + /// Name of icon asset + final String? assetName; + + /// Width of icon + final double? width; + + /// Height of icon + final double? height; + + /// Color of icon + final Color? color; + + @override + Widget build(BuildContext context) { + final iconTheme = IconTheme.of(context); + final color = this.color ?? iconTheme.color; + final width = this.width ?? iconTheme.size; + final height = this.height ?? iconTheme.size; + + return StreamSvgIcon( + assetName: assetName, + width: width, + height: height, + color: color, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/swipeable.dart b/packages/stream_chat_flutter/lib/src/misc/swipeable.dart similarity index 72% rename from packages/stream_chat_flutter/lib/src/swipeable.dart rename to packages/stream_chat_flutter/lib/src/misc/swipeable.dart index 8d36f36c..ded8520b 100644 --- a/packages/stream_chat_flutter/lib/src/swipeable.dart +++ b/packages/stream_chat_flutter/lib/src/misc/swipeable.dart @@ -3,9 +3,12 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// Widget to make a swipeable tile +/// {@template swipeable} +/// A swipeable tile in a list. Swiping on the tile will reveal actions that +/// can be taken. +/// {@endtemplate} class Swipeable extends StatefulWidget { - /// Constructor for creating a [Swipeable] widget + /// {@macro swipeable} const Swipeable({ super.key, required this.child, @@ -22,13 +25,13 @@ class Swipeable extends StatefulWidget { /// Background icon after swipe final Widget backgroundIcon; - /// Callback when swipe starts + /// The action to perform when the swipe starts final VoidCallback? onSwipeStart; - /// Callback when swipe is cancelled + /// The action to perform when the swipe is cancelled final VoidCallback? onSwipeCancel; - /// Callback when swipe ends + /// The action to perform when the swipe ends final VoidCallback? onSwipeEnd; /// Threshold for swipe @@ -129,41 +132,42 @@ class _SwipeableState extends State with TickerProviderStateMixin { } @override - Widget build(BuildContext context) => GestureDetector( - onHorizontalDragStart: _handleDragStart, - onHorizontalDragUpdate: _handleDragUpdate, - onHorizontalDragEnd: _handleDragEnd, - behavior: HitTestBehavior.opaque, - child: Stack( - alignment: Alignment.center, - fit: StackFit.passthrough, - children: [ - SlideTransition( - position: _iconTransitionAnimation, - child: Row( - children: [ - FadeTransition( - opacity: _iconFadeAnimation, - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: - StreamChatTheme.of(context).colorTheme.disabled, - ), + Widget build(BuildContext context) { + return GestureDetector( + onHorizontalDragStart: _handleDragStart, + onHorizontalDragUpdate: _handleDragUpdate, + onHorizontalDragEnd: _handleDragEnd, + behavior: HitTestBehavior.opaque, + child: Stack( + alignment: Alignment.center, + fit: StackFit.passthrough, + children: [ + SlideTransition( + position: _iconTransitionAnimation, + child: Row( + children: [ + FadeTransition( + opacity: _iconFadeAnimation, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: StreamChatTheme.of(context).colorTheme.disabled, ), - child: widget.backgroundIcon, ), + child: widget.backgroundIcon, ), - ], - ), + ), + ], ), - SlideTransition( - position: _moveAnimation, - child: widget.child, - ), - ], - ), - ); + ), + SlideTransition( + position: _moveAnimation, + child: widget.child, + ), + ], + ), + ); + } } diff --git a/packages/stream_chat_flutter/lib/src/system_message.dart b/packages/stream_chat_flutter/lib/src/misc/system_message.dart similarity index 66% rename from packages/stream_chat_flutter/lib/src/system_message.dart rename to packages/stream_chat_flutter/lib/src/misc/system_message.dart index fa4033da..7b5940c5 100644 --- a/packages/stream_chat_flutter/lib/src/system_message.dart +++ b/packages/stream_chat_flutter/lib/src/misc/system_message.dart @@ -1,28 +1,21 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@macro system_message} -@Deprecated("Use 'StreamSystemMessage' instead") -typedef SystemMessage = StreamSystemMessage; - -/// {@template system_message} -/// It shows a widget for the message with a system message type. +/// {@template streamSystemMessage} /// {@endtemplate} class StreamSystemMessage extends StatelessWidget { - /// Constructor for creating a [StreamSystemMessage] + /// {@macro streamSystemMessage} const StreamSystemMessage({ super.key, required this.message, this.onMessageTap, }); - /// This message + /// The message to display. final Message message; - /// The function called when tapping on the message - /// when the message is not failed - // TODO: Convert it to a simple OnTap instead. + /// The action to perform when tapping on the message. final void Function(Message)? onMessageTap; @override diff --git a/packages/stream_chat_flutter/lib/src/thread_header.dart b/packages/stream_chat_flutter/lib/src/misc/thread_header.dart similarity index 84% rename from packages/stream_chat_flutter/lib/src/thread_header.dart rename to packages/stream_chat_flutter/lib/src/misc/thread_header.dart index 85554eff..82caa9b2 100644 --- a/packages/stream_chat_flutter/lib/src/thread_header.dart +++ b/packages/stream_chat_flutter/lib/src/misc/thread_header.dart @@ -1,17 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@macro thread_header} -@Deprecated("Use 'StreamThreadHeader' instead") -typedef ThreadHeader = StreamThreadHeader; - -/// {@template thread_header} +/// {@template streamThreadHeader} /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/thread_header.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/thread_header_paint.png) /// -/// It shows the current thread information. +/// Shows information about the current message thread. /// /// ```dart /// class ThreadPage extends StatelessWidget { @@ -48,23 +44,23 @@ typedef ThreadHeader = StreamThreadHeader; /// Usually you would use this widget as an [AppBar] inside a [Scaffold]. /// However you can also use it as a normal widget. /// -/// Make sure to have a [StreamChannel] ancestor in order to provide the +/// A [StreamChannel] ancestor is required in order to provide the /// information about the channel. +/// /// Every part of the widget uses a [StreamBuilder] to render the channel /// information as soon as it updates. /// /// By default the widget shows a backButton that calls [Navigator.pop]. -/// You can disable this button using the [showBackButton] property of just -/// override the behaviour -/// with [onBackPressed]. +/// You can disable this button using the [showBackButton] property. +/// Alternatively, you can override the behavior with [onBackPressed]. /// -/// The widget components render the ui based on the first ancestor of type -/// [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property. -/// Modify it to change the widget appearance. +/// The UI is rendered based on the first ancestor of type [StreamChatTheme] +/// and the [ChannelTheme.channelHeaderTheme] property. Modify it to change +/// the widget's appearance. /// {@endtemplate} class StreamThreadHeader extends StatelessWidget implements PreferredSizeWidget { - /// Instantiate a new ThreadHeader + /// {@macro streamThreadHeader} const StreamThreadHeader({ super.key, required this.parent, @@ -81,14 +77,17 @@ class StreamThreadHeader extends StatelessWidget this.elevation = 1, }) : preferredSize = const Size.fromHeight(kToolbarHeight); - /// True if this header shows the leading back button + /// Whether to show the leading back button. + /// + /// Defaults to `true`. final bool showBackButton; - /// Callback to call when pressing the back button. + /// The action to perform when pressing the back button. + /// /// By default it calls [Navigator.pop] final VoidCallback? onBackPressed; - /// Callback to call when the title is tapped. + /// The action to perform when the title is tapped. final VoidCallback? onTitleTap; /// The message parent of this thread @@ -106,11 +105,12 @@ class StreamThreadHeader extends StatelessWidget /// Leading widget final Widget? leading; - /// AppBar actions + /// {@macro flutter.material.appbar.actions} final List? actions; - /// If true the typing indicator will be rendered - /// if a user is typing in this thread + /// Whether to show the typing indicator if users are currently typing. + /// + /// Defaults to `true`. final bool showTypingIndicator; /// The background color of this [StreamThreadHeader]. @@ -159,9 +159,9 @@ class StreamThreadHeader extends StatelessWidget leading: leading ?? (showBackButton ? StreamBackButton( - cid: StreamChannel.of(context).channel.cid, + channelId: StreamChannel.of(context).channel.cid, onPressed: onBackPressed, - showUnreads: true, + showUnreadCount: true, ) : const SizedBox()), backgroundColor: backgroundColor ?? channelHeaderTheme.color, diff --git a/packages/stream_chat_flutter/lib/src/visible_footnote.dart b/packages/stream_chat_flutter/lib/src/misc/visible_footnote.dart similarity index 71% rename from packages/stream_chat_flutter/lib/src/visible_footnote.dart rename to packages/stream_chat_flutter/lib/src/misc/visible_footnote.dart index 80fac2e0..998a8cb5 100644 --- a/packages/stream_chat_flutter/lib/src/visible_footnote.dart +++ b/packages/stream_chat_flutter/lib/src/misc/visible_footnote.dart @@ -1,16 +1,15 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@macro visible_footnote} -@Deprecated("Use 'StreamVisibleFootnote' instead") -typedef VisibleFootnote = StreamVisibleFootnote; - -/// {@template visible_footnote} -/// Widget for displaying a footnote +/// {@template streamVisibleFootnote} +/// Informs the user about a [StreamMessageWidget]'s visibility to the current +/// user. +/// +/// Used in [StreamGiphyAttachment]. /// {@endtemplate} class StreamVisibleFootnote extends StatelessWidget { - /// Constructor for creating a [StreamVisibleFootnote] + /// {@macro streamVisibleFootnote} const StreamVisibleFootnote({super.key}); @override diff --git a/packages/stream_chat_flutter/lib/src/multi_overlay.dart b/packages/stream_chat_flutter/lib/src/multi_overlay.dart deleted file mode 100644 index ef3757c8..00000000 --- a/packages/stream_chat_flutter/lib/src/multi_overlay.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_portal/flutter_portal.dart'; - -/// {@macro multi_overlay} -@Deprecated("Use 'StreamMultiOverlay' instead") -typedef MultiOverlay = StreamMultiOverlay; - -/// {@template multi_overlay} -/// Widget that renders a single overlay widget from a list of [overlayOptions] -/// It shows the first one that is visible -/// {@endtemplate} -class StreamMultiOverlay extends StatelessWidget { - /// Constructs a new MultiOverlay widget - /// [overlayOptions] - the list of overlay options - /// [overlayAnchor] - the anchor relative to the overlay - /// [childAnchor] - the anchor relative to the child - /// [child] - the child widget - const StreamMultiOverlay({ - super.key, - required this.overlayOptions, - required this.child, - required this.overlayAnchor, - required this.childAnchor, - }); - - /// The list of overlay options - final List overlayOptions; - - /// The child widget - final Widget child; - - /// The anchor relative to the overlay - final Alignment? overlayAnchor; - - /// The anchor relative to the child - final Alignment? childAnchor; - - @override - Widget build(BuildContext context) { - final visibleOverlay = - overlayOptions.firstWhereOrNull((element) => element.visible); - - return PortalTarget( - anchor: Aligned( - follower: overlayAnchor ?? Alignment.center, - target: childAnchor ?? Alignment.center, - ), - visible: visibleOverlay != null, - portalFollower: visibleOverlay?.widget, - child: child, - ); - } -} - -/// Class that contains the parameters for building an overlay entry -class OverlayOptions { - /// Constructs a new overlay options object - /// [visible] - the visibility of the overlay - /// [widget] - the widget to be displayed - OverlayOptions({ - required this.visible, - required this.widget, - }); - - /// the visibility of the overlay - final bool visible; - - /// the widget to be displayed - final Widget widget; -} diff --git a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart deleted file mode 100644 index 44122b65..00000000 --- a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart +++ /dev/null @@ -1,292 +0,0 @@ -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:video_player/video_player.dart'; - -/// Widget builder for quoted message attachment thumnail -typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function( - BuildContext, - Attachment, -); - -/// Widget for the quoted message. -@Deprecated("Use 'StreamQuotedMessageWidget' instead") -typedef QuotedMessageWidget = StreamQuotedMessageWidget; - -/// Widget for the quoted message. -class StreamQuotedMessageWidget extends StatelessWidget { - /// Creates a new instance of the widget. - const StreamQuotedMessageWidget({ - super.key, - required this.message, - required this.messageTheme, - this.reverse = false, - this.showBorder = false, - this.textLimit = 170, - this.attachmentThumbnailBuilders, - this.padding = const EdgeInsets.all(8), - this.onTap, - }); - - /// The message - final Message message; - - /// The message theme - final StreamMessageThemeData messageTheme; - - /// If true the widget will be mirrored - final bool reverse; - - /// If true the message will show a grey border - final bool showBorder; - - /// limit of the text message shown - final int textLimit; - - /// Map that defines a thumbnail builder for an attachment type - final Map? - attachmentThumbnailBuilders; - - /// Padding around the widget - final EdgeInsetsGeometry padding; - - /// Callback for tap on widget - final GestureTapCallback? onTap; - - bool get _hasAttachments => message.attachments.isNotEmpty; - - bool get _containsLinkAttachment => - message.attachments.any((element) => element.ogScrapeUrl != null); - - bool get _containsText => message.text?.isNotEmpty == true; - - @override - Widget build(BuildContext context) { - final children = [ - Flexible(child: _buildMessage(context)), - const SizedBox(width: 8), - if (message.user != null) _buildUserAvatar(), - ]; - return Padding( - padding: padding, - child: InkWell( - onTap: onTap, - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: reverse ? children.reversed.toList() : children, - ), - ), - ); - } - - Widget _buildMessage(BuildContext context) { - final isOnlyEmoji = message.text!.isOnlyEmoji; - var msg = _hasAttachments && !_containsText - ? message.copyWith(text: message.attachments.last.title ?? '') - : message; - if (msg.text!.length > textLimit) { - msg = msg.copyWith(text: '${msg.text!.substring(0, textLimit - 3)}...'); - } - - final children = [ - if (_hasAttachments) _parseAttachments(context), - if (msg.text!.isNotEmpty) - Flexible( - child: StreamMessageText( - message: msg, - messageTheme: isOnlyEmoji && _containsText - ? messageTheme.copyWith( - messageTextStyle: messageTheme.messageTextStyle?.copyWith( - fontSize: 32, - ), - ) - : messageTheme.copyWith( - messageTextStyle: messageTheme.messageTextStyle?.copyWith( - fontSize: 12, - ), - ), - ), - ), - ].insertBetween(const SizedBox(width: 8)); - - return Container( - decoration: BoxDecoration( - color: _getBackgroundColor(context), - border: showBorder - ? Border.all( - color: StreamChatTheme.of(context).colorTheme.disabled, - ) - : null, - borderRadius: BorderRadius.only( - topRight: const Radius.circular(12), - topLeft: const Radius.circular(12), - bottomRight: reverse ? const Radius.circular(12) : Radius.zero, - bottomLeft: reverse ? Radius.zero : const Radius.circular(12), - ), - ), - padding: const EdgeInsets.all(8), - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: - reverse ? MainAxisAlignment.end : MainAxisAlignment.start, - children: reverse ? children.reversed.toList() : children, - ), - ); - } - - Widget _buildUrlAttachment(Attachment attachment) { - const size = Size(32, 32); - if (attachment.thumbUrl != null) { - return Container( - height: size.height, - width: size.width, - decoration: BoxDecoration( - image: DecorationImage( - fit: BoxFit.cover, - image: CachedNetworkImageProvider( - attachment.thumbUrl!, - ), - ), - ), - ); - } - return const AttachmentError(size: size); - } - - Widget _parseAttachments(BuildContext context) { - Widget child; - Attachment attachment; - if (_containsLinkAttachment) { - attachment = message.attachments.firstWhere( - (element) => element.ogScrapeUrl != null, - ); - child = _buildUrlAttachment(attachment); - } else { - QuotedMessageAttachmentThumbnailBuilder? attachmentBuilder; - attachment = message.attachments.last; - if (attachmentThumbnailBuilders?.containsKey(attachment.type) == true) { - attachmentBuilder = attachmentThumbnailBuilders![attachment.type]; - } - attachmentBuilder = _defaultAttachmentBuilder[attachment.type]; - if (attachmentBuilder == null) { - child = const Offstage(); - } else { - child = attachmentBuilder(context, attachment); - } - } - child = AbsorbPointer(child: child); - return Material( - clipBehavior: Clip.hardEdge, - type: MaterialType.transparency, - shape: attachment.type == 'file' ? null : _getDefaultShape(context), - child: child, - ); - } - - ShapeBorder _getDefaultShape(BuildContext context) => RoundedRectangleBorder( - side: const BorderSide(width: 0, color: Colors.transparent), - borderRadius: BorderRadius.circular(8), - ); - - Widget _buildUserAvatar() => StreamUserAvatar( - user: message.user!, - constraints: const BoxConstraints.tightFor( - height: 24, - width: 24, - ), - showOnlineStatus: false, - ); - - Map - get _defaultAttachmentBuilder => { - 'image': (_, attachment) => StreamImageAttachment( - attachment: attachment, - message: message, - messageTheme: messageTheme, - size: const Size(32, 32), - ), - 'video': (_, attachment) => _VideoAttachmentThumbnail( - key: ValueKey(attachment.assetUrl), - attachment: attachment, - ), - 'giphy': (_, attachment) { - const size = Size(32, 32); - return CachedNetworkImage( - height: size.height, - width: size.width, - placeholder: (_, __) => SizedBox( - width: size.width, - height: size.height, - child: const Center( - child: CircularProgressIndicator(), - ), - ), - imageUrl: attachment.thumbUrl ?? - attachment.imageUrl ?? - attachment.assetUrl!, - errorWidget: (context, url, error) => - const AttachmentError(size: size), - fit: BoxFit.cover, - ); - }, - 'file': (_, attachment) => SizedBox( - height: 32, - width: 32, - child: getFileTypeImage( - attachment.extraData['mime_type'] as String?, - ), - ), - }; - - Color? _getBackgroundColor(BuildContext context) { - if (_containsLinkAttachment) { - return messageTheme.linkBackgroundColor; - } - return messageTheme.messageBackgroundColor; - } -} - -class _VideoAttachmentThumbnail extends StatefulWidget { - const _VideoAttachmentThumbnail({ - super.key, - required this.attachment, - }); - - final Attachment attachment; - - @override - _VideoAttachmentThumbnailState createState() => - _VideoAttachmentThumbnailState(); -} - -class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> { - late VideoPlayerController _controller; - - @override - void initState() { - super.initState(); - _controller = VideoPlayerController.network(widget.attachment.assetUrl!) - ..initialize().then((_) { - // ignore: no-empty-block - setState(() {}); //when your thumbnail will show. - }); - } - - @override - void dispose() { - super.dispose(); - _controller.dispose(); - } - - @override - Widget build(BuildContext context) => SizedBox( - height: 32, - width: 32, - child: _controller.value.isInitialized - ? VideoPlayer(_controller) - : const CircularProgressIndicator(), - ); -} diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_grid_tile.dart b/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_grid_tile.dart similarity index 100% rename from packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_grid_tile.dart rename to packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_grid_tile.dart diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_grid_view.dart b/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_grid_view.dart similarity index 94% rename from packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_grid_view.dart rename to packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_grid_view.dart index ab3089d8..fe40a02a 100644 --- a/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_grid_view.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_grid_view.dart @@ -1,16 +1,11 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; -import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/channel_scroll_view/stream_channel_grid_tile.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_empty_widget.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_error_widget.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_indexed_widget_builder.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_error.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_indicator.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_loading_widget.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Default grid delegate for [StreamChannelGridView]. const defaultChannelGridViewDelegate = diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_list_tile.dart b/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_tile.dart similarity index 81% rename from packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_list_tile.dart rename to packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_tile.dart index aa6511d8..daa672ea 100644 --- a/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_tile.dart @@ -1,6 +1,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// A widget that displays a channel preview. @@ -30,6 +30,8 @@ class StreamChannelListTile extends StatelessWidget { this.contentPadding = const EdgeInsets.symmetric(horizontal: 8), this.unreadIndicatorBuilder, this.sendingIndicatorBuilder, + this.selected = false, + this.selectedTileColor, }) : assert( channel.state != null, 'Channel ${channel.id} is not initialized', @@ -92,6 +94,12 @@ class StreamChannelListTile extends StatelessWidget { /// status using [Message.status]. final Widget Function(BuildContext, Message)? sendingIndicatorBuilder; + /// True if the tile is in a selected state. + final bool selected; + + /// The color of the tile in selected state. + final Color? selectedTileColor; + /// Creates a copy of this tile but with the given fields replaced with /// the new values. StreamChannelListTile copyWith({ @@ -104,18 +112,33 @@ class StreamChannelListTile extends StatelessWidget { VoidCallback? onLongPress, VisualDensity? visualDensity, EdgeInsetsGeometry? contentPadding, - }) => - StreamChannelListTile( - key: key ?? this.key, - channel: channel ?? this.channel, - leading: leading ?? this.leading, - title: title ?? this.title, - subtitle: subtitle ?? this.subtitle, - onTap: onTap ?? this.onTap, - onLongPress: onLongPress ?? this.onLongPress, - visualDensity: visualDensity ?? this.visualDensity, - contentPadding: contentPadding ?? this.contentPadding, - ); + bool? selected, + Widget Function(BuildContext, Message)? sendingIndicatorBuilder, + Color? tileColor, + Color? selectedTileColor, + WidgetBuilder? unreadIndicatorBuilder, + Widget? trailing, + }) { + return StreamChannelListTile( + key: key ?? this.key, + channel: channel ?? this.channel, + leading: leading ?? this.leading, + title: title ?? this.title, + subtitle: subtitle ?? this.subtitle, + onTap: onTap ?? this.onTap, + onLongPress: onLongPress ?? this.onLongPress, + visualDensity: visualDensity ?? this.visualDensity, + contentPadding: contentPadding ?? this.contentPadding, + sendingIndicatorBuilder: + sendingIndicatorBuilder ?? this.sendingIndicatorBuilder, + tileColor: tileColor ?? this.tileColor, + trailing: trailing ?? this.trailing, + unreadIndicatorBuilder: + unreadIndicatorBuilder ?? this.unreadIndicatorBuilder, + selected: selected ?? this.selected, + selectedTileColor: selectedTileColor ?? this.selectedTileColor, + ); + } @override Widget build(BuildContext context) { @@ -160,6 +183,9 @@ class StreamChannelListTile extends StatelessWidget { contentPadding: contentPadding, leading: leading, tileColor: tileColor, + selected: selected, + selectedTileColor: selectedTileColor ?? + StreamChatTheme.of(context).colorTheme.borders, title: Row( children: [ Expanded(child: title), @@ -200,14 +226,6 @@ class StreamChannelListTile extends StatelessWidget { return const Offstage(); } - final memberReadCount = channelState.read.where((it) { - return it.user.id != currentUser.id && - (it.lastRead.isAfter(lastMessage.createdAt) || - it.lastRead.isAtSameMomentAs( - lastMessage.createdAt, - )); - }).length; - return Padding( padding: const EdgeInsets.only(right: 4), child: @@ -215,9 +233,9 @@ class StreamChannelListTile extends StatelessWidget { StreamSendingIndicator( message: lastMessage, size: channelPreviewTheme.indicatorIconSize, - isMessageRead: memberReadCount >= - // Subtract one for the current user. - (channel.memberCount ?? 0) - 1, + isMessageRead: channelState + .currentUserRead!.lastRead + .isAfter(lastMessage.createdAt), ), ); }, @@ -327,7 +345,7 @@ class ChannelListTileSubtitle extends StatelessWidget { } /// A widget that displays the last message of a channel. -class ChannelLastMessageText extends StatelessWidget { +class ChannelLastMessageText extends StatefulWidget { /// Creates a new instance of [ChannelLastMessageText] widget. ChannelLastMessageText({ super.key, @@ -344,21 +362,32 @@ class ChannelLastMessageText extends StatelessWidget { /// The style of the text displayed final TextStyle? textStyle; + @override + State createState() => _ChannelLastMessageTextState(); +} + +class _ChannelLastMessageTextState extends State { + Message? _lastMessage; + @override Widget build(BuildContext context) => BetterStreamBuilder>( - stream: channel.state!.messagesStream, - initialData: channel.state!.messages, + stream: widget.channel.state!.messagesStream, + initialData: widget.channel.state!.messages, builder: (context, messages) { final lastMessage = messages.lastWhereOrNull( (m) => !m.shadowed && !m.isDeleted, ); - if (lastMessage == null) return const Offstage(); + if (widget.channel.state?.isUpToDate == true) { + _lastMessage = lastMessage; + } + + if (_lastMessage == null) return const Offstage(); return StreamMessagePreviewText( - message: lastMessage, - textStyle: textStyle, - language: channel.client.state.currentUser?.language, + message: _lastMessage!, + textStyle: widget.textStyle, + language: widget.channel.client.state.currentUser?.language, ); }, ); diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_list_view.dart b/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_view.dart similarity index 75% rename from packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_list_view.dart rename to packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_view.dart index a45ae28b..c1eae970 100644 --- a/packages/stream_chat_flutter/lib/src/v4/scroll_view/channel_scroll_view/stream_channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_view.dart @@ -1,17 +1,11 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; -import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/channel_scroll_view/stream_channel_list_tile.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_empty_widget.dart'; - -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_error_widget.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_indexed_widget_builder.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_error.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_indicator.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_loading_widget.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Default separator builder for [StreamChannelListView]. Widget defaultChannelListViewSeparatorBuilder( @@ -29,7 +23,7 @@ typedef StreamChannelListViewIndexedWidgetBuilder /// A [ListView] that shows a list of [Channel]s, /// it uses [StreamChannelListTile] as a default item. /// -/// This is the new version of [ChannelListView] that uses +/// This is the new version of [StreamChannelListView] that uses /// [StreamChannelListController]. /// /// Example: @@ -286,89 +280,90 @@ class StreamChannelListView extends StatelessWidget { final Clip clipBehavior; @override - Widget build(BuildContext context) => PagedValueListView( - scrollDirection: scrollDirection, - padding: padding, - physics: physics, - reverse: reverse, - controller: controller, - scrollController: scrollController, - primary: primary, - shrinkWrap: shrinkWrap, - addAutomaticKeepAlives: addAutomaticKeepAlives, - addRepaintBoundaries: addRepaintBoundaries, - addSemanticIndexes: addSemanticIndexes, - keyboardDismissBehavior: keyboardDismissBehavior, - restorationId: restorationId, - dragStartBehavior: dragStartBehavior, - cacheExtent: cacheExtent, - clipBehavior: clipBehavior, - loadMoreTriggerIndex: loadMoreTriggerIndex, - separatorBuilder: separatorBuilder, - itemBuilder: (context, channels, index) { - final channel = channels[index]; - final onTap = onChannelTap; - final onLongPress = onChannelLongPress; + Widget build(BuildContext context) { + return PagedValueListView( + scrollDirection: scrollDirection, + padding: padding, + physics: physics, + reverse: reverse, + controller: controller, + scrollController: scrollController, + primary: primary, + shrinkWrap: shrinkWrap, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + keyboardDismissBehavior: keyboardDismissBehavior, + restorationId: restorationId, + dragStartBehavior: dragStartBehavior, + cacheExtent: cacheExtent, + clipBehavior: clipBehavior, + loadMoreTriggerIndex: loadMoreTriggerIndex, + separatorBuilder: separatorBuilder, + itemBuilder: (context, channels, index) { + final channel = channels[index]; + final onTap = onChannelTap; + final onLongPress = onChannelLongPress; - final streamChannelListTile = StreamChannelListTile( - channel: channel, - onTap: onTap == null ? null : () => onTap(channel), - onLongPress: - onLongPress == null ? null : () => onLongPress(channel), - ); + final streamChannelListTile = StreamChannelListTile( + channel: channel, + onTap: onTap == null ? null : () => onTap(channel), + onLongPress: onLongPress == null ? null : () => onLongPress(channel), + ); - return itemBuilder?.call( - context, - channels, - index, - streamChannelListTile, - ) ?? - streamChannelListTile; - }, - emptyBuilder: (context) { - final chatThemeData = StreamChatTheme.of(context); - return emptyBuilder?.call(context) ?? - Center( - child: Padding( - padding: const EdgeInsets.all(8), - child: StreamScrollViewEmptyWidget( - emptyIcon: StreamSvgIcon.message( - size: 148, - color: chatThemeData.colorTheme.disabled, - ), - emptyTitle: Text( - context.translations.letsStartChattingLabel, - style: chatThemeData.textTheme.headline, - ), + return itemBuilder?.call( + context, + channels, + index, + streamChannelListTile, + ) ?? + streamChannelListTile; + }, + emptyBuilder: (context) { + final chatThemeData = StreamChatTheme.of(context); + return emptyBuilder?.call(context) ?? + Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: StreamScrollViewEmptyWidget( + emptyIcon: StreamSvgIcon.message( + size: 148, + color: chatThemeData.colorTheme.disabled, + ), + emptyTitle: Text( + context.translations.letsStartChattingLabel, + style: chatThemeData.textTheme.headline, ), ), - ); - }, - loadMoreErrorBuilder: (context, error) => - StreamScrollViewLoadMoreError.list( - onTap: controller.retry, - error: Text(context.translations.loadingChannelsError), - ), - loadMoreIndicatorBuilder: (context) => const Center( - child: Padding( - padding: EdgeInsets.all(16), - child: StreamScrollViewLoadMoreIndicator(), - ), - ), - loadingBuilder: (context) => - loadingBuilder?.call(context) ?? - const Center( - child: StreamScrollViewLoadingWidget(), - ), - errorBuilder: (context, error) => - errorBuilder?.call(context, error) ?? - Center( - child: StreamScrollViewErrorWidget( - errorTitle: Text(context.translations.loadingChannelsError), - onRetryPressed: controller.refresh, ), + ); + }, + loadMoreErrorBuilder: (context, error) => + StreamScrollViewLoadMoreError.list( + onTap: controller.retry, + error: Text(context.translations.loadingChannelsError), + ), + loadMoreIndicatorBuilder: (context) => const Center( + child: Padding( + padding: EdgeInsets.all(16), + child: StreamScrollViewLoadMoreIndicator(), + ), + ), + loadingBuilder: (context) => + loadingBuilder?.call(context) ?? + const Center( + child: StreamScrollViewLoadingWidget(), + ), + errorBuilder: (context, error) => + errorBuilder?.call(context, error) ?? + Center( + child: StreamScrollViewErrorWidget( + errorTitle: Text(context.translations.loadingChannelsError), + onRetryPressed: controller.refresh, ), - ); + ), + ); + } } /// A widget that is used to display a separator between diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/member_scroll_view/stream_member_grid_view.dart b/packages/stream_chat_flutter/lib/src/scroll_view/member_scroll_view/stream_member_grid_view.dart new file mode 100644 index 00000000..5cddf53d --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/scroll_view/member_scroll_view/stream_member_grid_view.dart @@ -0,0 +1,397 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// Default grid delegate for [StreamMemberGridView]. +const defaultMemberGridViewDelegate = + SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4); + +/// Signature for the item builder that creates the children of the +/// [StreamMemberGridView]. +typedef StreamMemberGridViewIndexedWidgetBuilder + = StreamScrollViewIndexedWidgetBuilder; + +/// Signature for the member grid tile, currently equal to [StreamUserGridTile]. +typedef StreamMemberGridTile = StreamUserGridTile; + +/// A [GridView] that shows a grid of [Member]s, +/// it uses [StreamMemberGridTile] as a default item. +/// +/// Example: +/// +/// ```dart +/// StreamMemberGridView( +/// controller: controller, +/// onMemberTap: (member) { +/// // Handle member tap event +/// }, +/// onMemberLongPress: (member) { +/// // Handle member long press event +/// }, +/// ) +/// ``` +/// +/// See also: +/// * [StreamMemberGridTile] +/// * [StreamMemberListController] +class StreamMemberGridView extends StatelessWidget { + /// Creates a new instance of [StreamMemberGridView]. + const StreamMemberGridView({ + super.key, + required this.controller, + this.gridDelegate = defaultMemberGridViewDelegate, + this.itemBuilder, + this.emptyBuilder, + this.loadMoreErrorBuilder, + this.loadMoreIndicatorBuilder, + this.loadingBuilder, + this.errorBuilder, + this.onMemberTap, + this.onMemberLongPress, + this.loadMoreTriggerIndex = 3, + this.scrollDirection = Axis.vertical, + this.reverse = false, + this.scrollController, + this.primary, + this.physics, + this.shrinkWrap = false, + this.padding, + this.addAutomaticKeepAlives = true, + this.addRepaintBoundaries = true, + this.addSemanticIndexes = true, + this.cacheExtent, + this.semanticChildCount, + this.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.restorationId, + this.clipBehavior = Clip.hardEdge, + }); + + /// The [StreamMemberListController] used to control the grid of members. + final StreamMemberListController controller; + + /// A delegate that controls the layout of the children within + /// the [PagedValueGridView]. + final SliverGridDelegate gridDelegate; + + /// A builder that is called to build items in the [PagedValueGridView]. + final StreamMemberGridViewIndexedWidgetBuilder? itemBuilder; + + /// A builder that is called to build the empty state of the grid. + final WidgetBuilder? emptyBuilder; + + /// A builder that is called to build the load more error state of the grid. + final PagedValueScrollViewLoadMoreErrorBuilder? loadMoreErrorBuilder; + + /// A builder that is called to build the load more indicator of the grid. + final WidgetBuilder? loadMoreIndicatorBuilder; + + /// A builder that is called to build the loading state of the grid. + final WidgetBuilder? loadingBuilder; + + /// A builder that is called to build the error state of the grid. + final Widget Function(BuildContext, StreamChatError)? errorBuilder; + + /// Called when the member taps this grid tile. + final void Function(Member)? onMemberTap; + + /// Called when the member long-presses on this grid tile. + final void Function(Member)? onMemberLongPress; + + /// The index to take into account when triggering [controller.loadMore]. + final int loadMoreTriggerIndex; + + /// {@template flutter.widgets.scroll_view.scrollDirection} + /// The axis along which the scroll view scrolls. + /// + /// Defaults to [Axis.vertical]. + /// {@endtemplate} + final Axis scrollDirection; + + /// {@template flutter.widgets.scroll_view.reverse} + /// Whether the scroll view scrolls in the reading direction. + /// + /// For example, if the reading direction is left-to-right and + /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from + /// left to right when [reverse] is false and from right to left when + /// [reverse] is true. + /// + /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view + /// scrolls from top to bottom when [reverse] is false and from bottom to top + /// when [reverse] is true. + /// + /// Defaults to false. + /// {@endtemplate} + final bool reverse; + + /// {@template flutter.widgets.scroll_view.controller} + /// An object that can be used to control the position to which this scroll + /// view is scrolled. + /// + /// Must be null if [primary] is true. + /// + /// A [ScrollController] serves several purposes. It can be used to control + /// the initial scroll position (see [ScrollController.initialScrollOffset]). + /// It can be used to control whether the scroll view should automatically + /// save and restore its scroll position in the [PageStorage] (see + /// [ScrollController.keepScrollOffset]). It can be used to read the current + /// scroll position (see [ScrollController.offset]), or change it (see + /// [ScrollController.animateTo]). + /// {@endtemplate} + final ScrollController? scrollController; + + /// {@template flutter.widgets.scroll_view.primary} + /// Whether this is the primary scroll view associated with the parent + /// [PrimaryScrollController]. + /// + /// When this is true, the scroll view is scrollable even if it does not have + /// sufficient content to actually scroll. Otherwise, by default the user can + /// only scroll the view if it has sufficient content. See [physics]. + /// + /// Also when true, the scroll view is used for default [ScrollAction]s. If a + /// ScrollAction is not handled by + /// an otherwise focused part of the application, + /// the ScrollAction will be evaluated using this scroll view, for example, + /// when executing [Shortcuts] key events like page up and down. + /// + /// On iOS, this also identifies the scroll view that will scroll to top in + /// response to a tap in the status bar. + /// {@endtemplate} + /// + /// Defaults to true when [scrollDirection] is [Axis.vertical] and + /// [controller] is null. + final bool? primary; + + /// {@template flutter.widgets.scroll_view.physics} + /// How the scroll view should respond to user input. + /// + /// For example, determines how the scroll view continues to animate after the + /// user stops dragging the scroll view. + /// + /// Defaults to matching platform conventions. Furthermore, if [primary] is + /// false, then the user cannot scroll if there is insufficient content to + /// scroll, while if [primary] is true, they can always attempt to scroll. + /// + /// To force the scroll view to always be scrollable even if there is + /// insufficient content, as if [primary] was true but without necessarily + /// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics + /// object, as in: + /// + /// ```dart + /// physics: const AlwaysScrollableScrollPhysics(), + /// ``` + /// + /// To force the scroll view to use the default platform conventions and not + /// be scrollable if there is insufficient content, regardless of the value of + /// [primary], provide an explicit [ScrollPhysics] object, as in: + /// + /// ```dart + /// physics: const ScrollPhysics(), + /// ``` + /// + /// The physics can be changed dynamically (by providing a new object in a + /// subsequent build), but new physics will only take effect if the _class_ of + /// the provided object changes. Merely constructing a new instance with a + /// different configuration is insufficient to cause the physics to be + /// reapplied. (This is because the final object used is generated + /// dynamically, which can be relatively expensive, and it would be + /// inefficient to speculatively create this object each frame to see if the + /// physics should be updated.) + /// {@endtemplate} + /// + /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the + /// [ScrollPhysics] provided by that behavior will take precedence after + /// [physics]. + final ScrollPhysics? physics; + + /// {@template flutter.widgets.scroll_view.shrinkWrap} + /// Whether the extent of the scroll view in the [scrollDirection] should be + /// determined by the contents being viewed. + /// + /// If the scroll view does not shrink wrap, then the scroll view will expand + /// to the maximum allowed size in the [scrollDirection]. If the scroll view + /// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must + /// be true. + /// + /// Shrink wrapping the content of the scroll view is significantly more + /// expensive than expanding to the maximum allowed size because the content + /// can expand and contract during scrolling, which means the size of the + /// scroll view needs to be recomputed whenever the scroll position changes. + /// + /// Defaults to false. + /// {@endtemplate} + final bool shrinkWrap; + + /// The amount of space by which to inset the children. + final EdgeInsetsGeometry? padding; + + /// Whether to wrap each child in an [AutomaticKeepAlive]. + /// + /// Typically, children in lazy list are wrapped in [AutomaticKeepAlive] + /// widgets so that children can use [KeepAliveNotification]s to preserve + /// their state when they would otherwise be garbage collected off-screen. + /// + /// This feature (and [addRepaintBoundaries]) must be disabled if the children + /// are going to manually maintain their [KeepAlive] state. It may also be + /// more efficient to disable this feature if it is known ahead of time that + /// none of the children will ever try to keep themselves alive. + /// + /// Defaults to true. + final bool addAutomaticKeepAlives; + + /// Whether to wrap each child in a [RepaintBoundary]. + /// + /// Typically, children in a scrolling container are wrapped in repaint + /// boundaries so that they do not need to be repainted as the list scrolls. + /// If the children are easy to repaint (e.g., solid color blocks or a short + /// snippet of text), it might be more efficient to not add a repaint boundary + /// and simply repaint the children during scrolling. + /// + /// Defaults to true. + final bool addRepaintBoundaries; + + /// Whether to wrap each child in an [IndexedSemantics]. + /// + /// Typically, children in a scrolling container must be annotated with a + /// semantic index in order to generate the correct accessibility + /// announcements. This should only be set to false if the indexes have + /// already been provided by an [IndexedSemantics] widget. + /// + /// Defaults to true. + /// + /// See also: + /// + /// * [IndexedSemantics], for an explanation of how to manually + /// provide semantic indexes. + final bool addSemanticIndexes; + + /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} + final double? cacheExtent; + + /// The number of children that will contribute semantic information. + /// + /// Some subtypes of [ScrollView] can infer this value automatically. For + /// example [ListView] will use the number of widgets in the child list, + /// while the [ListView.separated] constructor will use half that amount. + /// + /// For [CustomScrollView] and other types which do not receive a builder + /// or list of widgets, the child count must be explicitly provided. If the + /// number is unknown or unbounded this should be left unset or set to null. + /// + /// See also: + /// + /// * [SemanticsConfiguration.scrollChildCount], + /// the corresponding semantics property. + final int? semanticChildCount; + + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + + /// {@template flutter.widgets.scroll_view.keyboardDismissBehavior} + /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will + /// dismiss the keyboard automatically. + /// {@endtemplate} + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + + /// {@macro flutter.widgets.scrollable.restorationId} + final String? restorationId; + + /// {@macro flutter.material.Material.clipBehavior} + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + + @override + Widget build(BuildContext context) { + return PagedValueGridView( + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + shrinkWrap: shrinkWrap, + padding: padding, + scrollController: scrollController, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + cacheExtent: cacheExtent, + semanticChildCount: semanticChildCount, + dragStartBehavior: dragStartBehavior, + keyboardDismissBehavior: keyboardDismissBehavior, + restorationId: restorationId, + clipBehavior: clipBehavior, + gridDelegate: gridDelegate, + itemBuilder: (context, members, index) { + final member = members[index]; + final onTap = onMemberTap; + final onLongPress = onMemberLongPress; + + final streamMemberGridTile = StreamMemberGridTile( + user: member.user!, + onTap: onTap == null ? null : () => onTap(member), + onLongPress: onLongPress == null ? null : () => onLongPress(member), + ); + + return itemBuilder?.call( + context, + members, + index, + streamMemberGridTile, + ) ?? + streamMemberGridTile; + }, + emptyBuilder: (context) { + final chatThemeData = StreamChatTheme.of(context); + return emptyBuilder?.call(context) ?? + Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: StreamScrollViewEmptyWidget( + emptyIcon: StreamSvgIcon.user( + size: 148, + color: chatThemeData.colorTheme.disabled, + ), + emptyTitle: Text( + context.translations.noUsersLabel, + style: chatThemeData.textTheme.headline, + ), + ), + ), + ); + }, + loadMoreErrorBuilder: (context, error) => + StreamScrollViewLoadMoreError.grid( + onTap: controller.retry, + error: Text( + context.translations.loadingUsersError, + textAlign: TextAlign.center, + ), + ), + loadMoreIndicatorBuilder: (context) => const Center( + child: Padding( + padding: EdgeInsets.all(16), + child: StreamScrollViewLoadMoreIndicator(), + ), + ), + loadingBuilder: (context) => + loadingBuilder?.call(context) ?? + const Center( + child: StreamScrollViewLoadingWidget(), + ), + errorBuilder: (context, error) => + errorBuilder?.call(context, error) ?? + Center( + child: StreamScrollViewErrorWidget( + errorTitle: Text(context.translations.loadingUsersError), + onRetryPressed: controller.refresh, + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/member_scroll_view/stream_member_list_view.dart b/packages/stream_chat_flutter/lib/src/scroll_view/member_scroll_view/stream_member_list_view.dart new file mode 100644 index 00000000..bc8abd12 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/scroll_view/member_scroll_view/stream_member_list_view.dart @@ -0,0 +1,365 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart'; +import 'package:stream_chat_flutter/src/utils/utils.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// Default separator builder for [StreamMemberListView]. +Widget defaultMemberListViewSeparatorBuilder( + BuildContext context, + List members, + int index, +) => + const StreamUserListSeparator(); + +/// Signature for the item builder that creates the children of the +/// [StreamMemberListView]. +typedef StreamMemberListViewIndexedWidgetBuilder + = StreamScrollViewIndexedWidgetBuilder; + +/// Signature for the member grid tile, currently equal to [StreamUserListTile]. +typedef StreamMemberListTile = StreamUserListTile; + +/// A [ListView] that shows a list of [Member]s, +/// it uses [StreamMemberListTile] as a default item. +/// +/// Example: +/// +/// ```dart +/// StreamMemberListView( +/// controller: controller, +/// onMemberTap: (member) { +/// // Handle member tap event +/// }, +/// onMemberLongPress: (member) { +/// // Handle member long press event +/// }, +/// ) +/// ``` +/// +/// See also: +/// * [StreamMemberListTile] +/// * [StreamMemberListController] +class StreamMemberListView extends StatelessWidget { + /// Creates a new instance of [StreamMemberListView]. + const StreamMemberListView({ + super.key, + required this.controller, + this.itemBuilder, + this.separatorBuilder = defaultMemberListViewSeparatorBuilder, + this.emptyBuilder, + this.loadingBuilder, + this.errorBuilder, + this.onMemberTap, + this.onMemberLongPress, + this.loadMoreTriggerIndex = 3, + this.scrollDirection = Axis.vertical, + this.reverse = false, + this.scrollController, + this.primary, + this.physics, + this.shrinkWrap = false, + this.padding, + this.addAutomaticKeepAlives = true, + this.addRepaintBoundaries = true, + this.addSemanticIndexes = true, + this.cacheExtent, + this.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.restorationId, + this.clipBehavior = Clip.hardEdge, + }); + + /// The [StreamMemberListController] used to control the list of members. + final StreamMemberListController controller; + + /// A builder that is called to build items in the [ListView]. + final StreamMemberListViewIndexedWidgetBuilder? itemBuilder; + + /// A builder that is called to build the list separator. + final PagedValueScrollViewIndexedWidgetBuilder separatorBuilder; + + /// A builder that is called to build the empty state of the list. + final WidgetBuilder? emptyBuilder; + + /// A builder that is called to build the loading state of the list. + final WidgetBuilder? loadingBuilder; + + /// A builder that is called to build the error state of the list. + final Widget Function(BuildContext, StreamChatError)? errorBuilder; + + /// Called when the member taps this list tile. + final void Function(Member)? onMemberTap; + + /// Called when the member long-presses on this list tile. + final void Function(Member)? onMemberLongPress; + + /// The index to take into account when triggering [controller.loadMore]. + final int loadMoreTriggerIndex; + + /// {@template flutter.widgets.scroll_view.scrollDirection} + /// The axis along which the scroll view scrolls. + /// + /// Defaults to [Axis.vertical]. + /// {@endtemplate} + final Axis scrollDirection; + + /// The amount of space by which to inset the children. + final EdgeInsetsGeometry? padding; + + /// Whether to wrap each child in an [AutomaticKeepAlive]. + /// + /// Typically, children in lazy list are wrapped in [AutomaticKeepAlive] + /// widgets so that children can use [KeepAliveNotification]s to preserve + /// their state when they would otherwise be garbage collected off-screen. + /// + /// This feature (and [addRepaintBoundaries]) must be disabled if the children + /// are going to manually maintain their [KeepAlive] state. It may also be + /// more efficient to disable this feature if it is known ahead of time that + /// none of the children will ever try to keep themselves alive. + /// + /// Defaults to true. + final bool addAutomaticKeepAlives; + + /// Whether to wrap each child in a [RepaintBoundary]. + /// + /// Typically, children in a scrolling container are wrapped in repaint + /// boundaries so that they do not need to be repainted as the list scrolls. + /// If the children are easy to repaint (e.g., solid color blocks or a short + /// snippet of text), it might be more efficient to not add a repaint boundary + /// and simply repaint the children during scrolling. + /// + /// Defaults to true. + final bool addRepaintBoundaries; + + /// Whether to wrap each child in an [IndexedSemantics]. + /// + /// Typically, children in a scrolling container must be annotated with a + /// semantic index in order to generate the correct accessibility + /// announcements. This should only be set to false if the indexes have + /// already been provided by an [IndexedSemantics] widget. + /// + /// Defaults to true. + /// + /// See also: + /// + /// * [IndexedSemantics], for an explanation of how to manually + /// provide semantic indexes. + final bool addSemanticIndexes; + + /// {@template flutter.widgets.scroll_view.reverse} + /// Whether the scroll view scrolls in the reading direction. + /// + /// For example, if [scrollDirection] is [Axis.vertical], then the scroll view + /// scrolls from top to bottom when [reverse] is false and from bottom to top + /// when [reverse] is true. + /// + /// Defaults to false. + /// {@endtemplate} + final bool reverse; + + /// {@template flutter.widgets.scroll_view.controller} + /// An object that can be used to control the position to which this scroll + /// view is scrolled. + /// + /// Must be null if [primary] is true. + /// + /// A [ScrollController] serves several purposes. It can be used to control + /// the initial scroll position (see [ScrollController.initialScrollOffset]). + /// It can be used to control whether the scroll view should automatically + /// save and restore its scroll position in the [PageStorage] (see + /// [ScrollController.keepScrollOffset]). It can be used to read the current + /// scroll position (see [ScrollController.offset]), or change it (see + /// [ScrollController.animateTo]). + /// {@endtemplate} + final ScrollController? scrollController; + + /// {@template flutter.widgets.scroll_view.primary} + /// Whether this is the primary scroll view associated with the parent + /// [PrimaryScrollController]. + /// + /// When this is true, the scroll view is scrollable even if it does not have + /// sufficient content to actually scroll. Otherwise, by default the user can + /// only scroll the view if it has sufficient content. See [physics]. + /// + /// Also when true, the scroll view is used for default [ScrollAction]s. If a + /// ScrollAction is not handled by an otherwise focused part of the + /// application, the ScrollAction will be evaluated using this scroll view, + /// for example, when executing [Shortcuts] key events like page up and down. + /// + /// On iOS, this also identifies the scroll view that will scroll to top in + /// response to a tap in the status bar. + /// {@endtemplate} + /// + /// Defaults to true when [scrollController] is null. + final bool? primary; + + /// {@template flutter.widgets.scroll_view.shrinkWrap} + /// Whether the extent of the scroll view in the [scrollDirection] should be + /// determined by the contents being viewed. + /// + /// If the scroll view does not shrink wrap, then the scroll view will expand + /// to the maximum allowed size in the [scrollDirection]. If the scroll view + /// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must + /// be true. + /// + /// Shrink wrapping the content of the scroll view is significantly more + /// expensive than expanding to the maximum allowed size because the content + /// can expand and contract during scrolling, which means the size of the + /// scroll view needs to be recomputed whenever the scroll position changes. + /// + /// Defaults to false. + /// {@endtemplate} + final bool shrinkWrap; + + /// {@template flutter.widgets.scroll_view.physics} + /// How the scroll view should respond to user input. + /// + /// For example, determines how the scroll view continues to animate after the + /// user stops dragging the scroll view. + /// + /// Defaults to matching platform conventions. Furthermore, if [primary] is + /// false, then the user cannot scroll if there is insufficient content to + /// scroll, while if [primary] is true, they can always attempt to scroll. + /// + /// To force the scroll view to always be scrollable even if there is + /// insufficient content, as if [primary] was true but without necessarily + /// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics + /// object, as in: + /// + /// ```dart + /// physics: const AlwaysScrollableScrollPhysics(), + /// ``` + /// + /// To force the scroll view to use the default platform conventions and not + /// be scrollable if there is insufficient content, regardless of the value of + /// [primary], provide an explicit [ScrollPhysics] object, as in: + /// + /// ```dart + /// physics: const ScrollPhysics(), + /// ``` + /// + /// The physics can be changed dynamically (by providing a new object in a + /// subsequent build), but new physics will only take effect if the _class_ of + /// the provided object changes. Merely constructing a new instance with a + /// different configuration is insufficient to cause the physics to be + /// reapplied. (This is because the final object used is generated + /// dynamically, which can be relatively expensive, and it would be + /// inefficient to speculatively create this object each frame to see if the + /// physics should be updated.) + /// {@endtemplate} + /// + /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the + /// [ScrollPhysics] provided by that behavior will take precedence after + /// [physics]. + final ScrollPhysics? physics; + + /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} + final double? cacheExtent; + + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + + /// {@template flutter.widgets.scroll_view.keyboardDismissBehavior} + /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will + /// dismiss the keyboard automatically. + /// {@endtemplate} + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + + /// {@macro flutter.widgets.scrollable.restorationId} + final String? restorationId; + + /// {@macro flutter.material.Material.clipBehavior} + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + + @override + Widget build(BuildContext context) => PagedValueListView( + scrollDirection: scrollDirection, + padding: padding, + physics: physics, + reverse: reverse, + controller: controller, + scrollController: scrollController, + primary: primary, + shrinkWrap: shrinkWrap, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + keyboardDismissBehavior: keyboardDismissBehavior, + restorationId: restorationId, + dragStartBehavior: dragStartBehavior, + cacheExtent: cacheExtent, + clipBehavior: clipBehavior, + loadMoreTriggerIndex: loadMoreTriggerIndex, + separatorBuilder: separatorBuilder, + itemBuilder: (context, members, index) { + final member = members[index]; + final onTap = onMemberTap; + final onLongPress = onMemberLongPress; + + final streamUserListTile = StreamMemberListTile( + user: member.user!, + onTap: onTap == null ? null : () => onTap(member), + onLongPress: onLongPress == null ? null : () => onLongPress(member), + ); + + return itemBuilder?.call( + context, + members, + index, + streamUserListTile, + ) ?? + streamUserListTile; + }, + emptyBuilder: (context) { + final chatThemeData = StreamChatTheme.of(context); + return emptyBuilder?.call(context) ?? + Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: StreamScrollViewEmptyWidget( + emptyIcon: StreamSvgIcon.user( + size: 148, + color: chatThemeData.colorTheme.disabled, + ), + emptyTitle: Text( + context.translations.noUsersLabel, + style: chatThemeData.textTheme.headline, + ), + ), + ), + ); + }, + loadMoreErrorBuilder: (context, error) => + StreamScrollViewLoadMoreError.list( + onTap: controller.retry, + error: Text(context.translations.loadingUsersError), + ), + loadMoreIndicatorBuilder: (context) => const Center( + child: Padding( + padding: EdgeInsets.all(16), + child: StreamScrollViewLoadMoreIndicator(), + ), + ), + loadingBuilder: (context) => + loadingBuilder?.call(context) ?? + const Center( + child: StreamScrollViewLoadingWidget(), + ), + errorBuilder: (context, error) => + errorBuilder?.call(context, error) ?? + Center( + child: StreamScrollViewErrorWidget( + errorTitle: Text(context.translations.loadingUsersError), + onRetryPressed: controller.refresh, + ), + ), + ); +} diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/message_search_scroll_view/stream_message_search_grid_view.dart b/packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_grid_view.dart similarity index 96% rename from packages/stream_chat_flutter/lib/src/v4/scroll_view/message_search_scroll_view/stream_message_search_grid_view.dart rename to packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_grid_view.dart index 355e3a32..db147f39 100644 --- a/packages/stream_chat_flutter/lib/src/v4/scroll_view/message_search_scroll_view/stream_message_search_grid_view.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_grid_view.dart @@ -1,11 +1,10 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; - -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_error_widget.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_error.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_indicator.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_loading_widget.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Default grid delegate for [StreamMessageSearchGridView]. diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/message_search_scroll_view/stream_message_search_list_tile.dart b/packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_list_tile.dart similarity index 99% rename from packages/stream_chat_flutter/lib/src/v4/scroll_view/message_search_scroll_view/stream_message_search_list_tile.dart rename to packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_list_tile.dart index a380c136..296061c4 100644 --- a/packages/stream_chat_flutter/lib/src/v4/scroll_view/message_search_scroll_view/stream_message_search_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_list_tile.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/utils/utils.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// A widget that displays a message search item. diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/message_search_scroll_view/stream_message_search_list_view.dart b/packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_list_view.dart similarity index 76% rename from packages/stream_chat_flutter/lib/src/v4/scroll_view/message_search_scroll_view/stream_message_search_list_view.dart rename to packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_list_view.dart index b1a184ca..3e2b0ba3 100644 --- a/packages/stream_chat_flutter/lib/src/v4/scroll_view/message_search_scroll_view/stream_message_search_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_list_view.dart @@ -2,11 +2,11 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_error_widget.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_error.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_indicator.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_loading_widget.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart'; +import 'package:stream_chat_flutter/src/utils/utils.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Default separator builder for [StreamMessageSearchListView]. @@ -283,90 +283,91 @@ class StreamMessageSearchListView extends StatelessWidget { final Clip clipBehavior; @override - Widget build(BuildContext context) => - PagedValueListView( - scrollDirection: scrollDirection, - padding: padding, - physics: physics, - reverse: reverse, - controller: controller, - scrollController: scrollController, - primary: primary, - shrinkWrap: shrinkWrap, - addAutomaticKeepAlives: addAutomaticKeepAlives, - addRepaintBoundaries: addRepaintBoundaries, - addSemanticIndexes: addSemanticIndexes, - keyboardDismissBehavior: keyboardDismissBehavior, - restorationId: restorationId, - dragStartBehavior: dragStartBehavior, - cacheExtent: cacheExtent, - clipBehavior: clipBehavior, - loadMoreTriggerIndex: loadMoreTriggerIndex, - separatorBuilder: separatorBuilder, - itemBuilder: (context, messageResponses, index) { - final messageResponse = messageResponses[index]; - final onTap = onMessageTap; - final onLongPress = onMessageLongPress; + Widget build(BuildContext context) { + return PagedValueListView( + scrollDirection: scrollDirection, + padding: padding, + physics: physics, + reverse: reverse, + controller: controller, + scrollController: scrollController, + primary: primary, + shrinkWrap: shrinkWrap, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + keyboardDismissBehavior: keyboardDismissBehavior, + restorationId: restorationId, + dragStartBehavior: dragStartBehavior, + cacheExtent: cacheExtent, + clipBehavior: clipBehavior, + loadMoreTriggerIndex: loadMoreTriggerIndex, + separatorBuilder: separatorBuilder, + itemBuilder: (context, messageResponses, index) { + final messageResponse = messageResponses[index]; + final onTap = onMessageTap; + final onLongPress = onMessageLongPress; - final streamMessageSearchListTile = StreamMessageSearchListTile( - messageResponse: messageResponse, - onTap: onTap == null ? null : () => onTap(messageResponse), - onLongPress: - onLongPress == null ? null : () => onLongPress(messageResponse), - ); + final streamMessageSearchListTile = StreamMessageSearchListTile( + messageResponse: messageResponse, + onTap: onTap == null ? null : () => onTap(messageResponse), + onLongPress: + onLongPress == null ? null : () => onLongPress(messageResponse), + ); - return itemBuilder?.call( - context, - messageResponses, - index, - streamMessageSearchListTile, - ) ?? - streamMessageSearchListTile; - }, - emptyBuilder: (context) { - final chatThemeData = StreamChatTheme.of(context); - return emptyBuilder?.call(context) ?? - Center( - child: Padding( - padding: const EdgeInsets.all(8), - child: StreamScrollViewEmptyWidget( - emptyIcon: StreamSvgIcon.message( - size: 148, - color: chatThemeData.colorTheme.disabled, - ), - emptyTitle: Text( - context.translations.emptyMessagesText, - style: chatThemeData.textTheme.headline, - ), + return itemBuilder?.call( + context, + messageResponses, + index, + streamMessageSearchListTile, + ) ?? + streamMessageSearchListTile; + }, + emptyBuilder: (context) { + final chatThemeData = StreamChatTheme.of(context); + return emptyBuilder?.call(context) ?? + Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: StreamScrollViewEmptyWidget( + emptyIcon: StreamSvgIcon.message( + size: 148, + color: chatThemeData.colorTheme.disabled, + ), + emptyTitle: Text( + context.translations.emptyMessagesText, + style: chatThemeData.textTheme.headline, ), ), - ); - }, - loadMoreErrorBuilder: (context, error) => - StreamScrollViewLoadMoreError.list( - onTap: controller.retry, - error: Text(context.translations.loadingMessagesError), - ), - loadMoreIndicatorBuilder: (context) => const Center( - child: Padding( - padding: EdgeInsets.all(16), - child: StreamScrollViewLoadMoreIndicator(), - ), - ), - loadingBuilder: (context) => - loadingBuilder?.call(context) ?? - const Center( - child: StreamScrollViewLoadingWidget(), - ), - errorBuilder: (context, error) => - errorBuilder?.call(context, error) ?? - Center( - child: StreamScrollViewErrorWidget( - errorTitle: Text(context.translations.loadingMessagesError), - onRetryPressed: controller.refresh, ), + ); + }, + loadMoreErrorBuilder: (context, error) => + StreamScrollViewLoadMoreError.list( + onTap: controller.retry, + error: Text(context.translations.loadingMessagesError), + ), + loadMoreIndicatorBuilder: (context) => const Center( + child: Padding( + padding: EdgeInsets.all(16), + child: StreamScrollViewLoadMoreIndicator(), + ), + ), + loadingBuilder: (context) => + loadingBuilder?.call(context) ?? + const Center( + child: StreamScrollViewLoadingWidget(), + ), + errorBuilder: (context, error) => + errorBuilder?.call(context, error) ?? + Center( + child: StreamScrollViewErrorWidget( + errorTitle: Text(context.translations.loadingMessagesError), + onRetryPressed: controller.refresh, ), - ); + ), + ); + } } /// A widget that is used to display a separator between diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery.dart b/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery.dart new file mode 100644 index 00000000..9b95f4b9 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery.dart @@ -0,0 +1,417 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:photo_manager/photo_manager.dart' + show AssetEntity, ThumbnailFormat, ThumbnailSize; + +import 'package:stream_chat_flutter/src/misc/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/scroll_view/photo_gallery/stream_photo_gallery_controller.dart'; +import 'package:stream_chat_flutter/src/scroll_view/photo_gallery/stream_photo_gallery_tile.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_empty_widget.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_indexed_widget_builder.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// Default grid delegate for [StreamPhotoGallery]. +const defaultStreamPhotoGalleryDelegate = + SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + mainAxisSpacing: 2, + crossAxisSpacing: 2, +); + +/// Signature for the item builder that creates the children of the +/// [StreamPhotoGallery]. +typedef StreamPhotoGalleryIndexedWidgetBuilder + = StreamScrollViewIndexedWidgetBuilder; + +/// Widget used to display a gallery of photos in the form of grid. +class StreamPhotoGallery extends StatelessWidget { + /// Creates a [StreamPhotoGallery] widget. + const StreamPhotoGallery({ + super.key, + required this.controller, + this.gridDelegate = defaultStreamPhotoGalleryDelegate, + this.itemBuilder, + this.emptyBuilder, + this.loadMoreErrorBuilder, + this.loadMoreIndicatorBuilder, + this.loadingBuilder, + this.errorBuilder, + this.onMediaTap, + this.onMediaLongPress, + this.loadMoreTriggerIndex = 3, + this.scrollDirection = Axis.vertical, + this.reverse = false, + this.scrollController, + this.primary, + this.physics, + this.shrinkWrap = false, + this.padding, + this.addAutomaticKeepAlives = true, + this.addRepaintBoundaries = true, + this.addSemanticIndexes = true, + this.cacheExtent, + this.semanticChildCount, + this.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.restorationId, + this.clipBehavior = Clip.hardEdge, + this.thumbnailSize = const ThumbnailSize(400, 400), + this.thumbnailFormat = ThumbnailFormat.jpeg, + this.thumbnailQuality = 100, + this.thumbnailScale = 1, + }); + + /// The [StreamPhotoGalleryController] used to control the grid of users. + final StreamPhotoGalleryController controller; + + /// A delegate that controls the layout of the children within + /// the [PagedValueGridView]. + final SliverGridDelegate gridDelegate; + + /// A builder that is called to build items in the [PagedValueGridView]. + final StreamPhotoGalleryIndexedWidgetBuilder? itemBuilder; + + /// A builder that is called to build the empty state of the grid. + final WidgetBuilder? emptyBuilder; + + /// A builder that is called to build the load more error state of the grid. + final PagedValueScrollViewLoadMoreErrorBuilder? loadMoreErrorBuilder; + + /// A builder that is called to build the load more indicator of the grid. + final WidgetBuilder? loadMoreIndicatorBuilder; + + /// A builder that is called to build the loading state of the grid. + final WidgetBuilder? loadingBuilder; + + /// A builder that is called to build the error state of the grid. + final Widget Function(BuildContext, StreamChatError)? errorBuilder; + + /// Called when the user taps this grid tile. + final void Function(AssetEntity)? onMediaTap; + + /// Called when the user long-presses on this grid tile. + final void Function(AssetEntity)? onMediaLongPress; + + /// The index to take into account when triggering [controller.loadMore]. + final int loadMoreTriggerIndex; + + /// {@template flutter.widgets.scroll_view.scrollDirection} + /// The axis along which the scroll view scrolls. + /// + /// Defaults to [Axis.vertical]. + /// {@endtemplate} + final Axis scrollDirection; + + /// {@template flutter.widgets.scroll_view.reverse} + /// Whether the scroll view scrolls in the reading direction. + /// + /// For example, if the reading direction is left-to-right and + /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from + /// left to right when [reverse] is false and from right to left when + /// [reverse] is true. + /// + /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view + /// scrolls from top to bottom when [reverse] is false and from bottom to top + /// when [reverse] is true. + /// + /// Defaults to false. + /// {@endtemplate} + final bool reverse; + + /// {@template flutter.widgets.scroll_view.controller} + /// An object that can be used to control the position to which this scroll + /// view is scrolled. + /// + /// Must be null if [primary] is true. + /// + /// A [ScrollController] serves several purposes. It can be used to control + /// the initial scroll position (see [ScrollController.initialScrollOffset]). + /// It can be used to control whether the scroll view should automatically + /// save and restore its scroll position in the [PageStorage] (see + /// [ScrollController.keepScrollOffset]). It can be used to read the current + /// scroll position (see [ScrollController.offset]), or change it (see + /// [ScrollController.animateTo]). + /// {@endtemplate} + final ScrollController? scrollController; + + /// {@template flutter.widgets.scroll_view.primary} + /// Whether this is the primary scroll view associated with the parent + /// [PrimaryScrollController]. + /// + /// When this is true, the scroll view is scrollable even if it does not have + /// sufficient content to actually scroll. Otherwise, by default the user can + /// only scroll the view if it has sufficient content. See [physics]. + /// + /// Also when true, the scroll view is used for default [ScrollAction]s. If a + /// ScrollAction is not handled by + /// an otherwise focused part of the application, + /// the ScrollAction will be evaluated using this scroll view, for example, + /// when executing [Shortcuts] key events like page up and down. + /// + /// On iOS, this also identifies the scroll view that will scroll to top in + /// response to a tap in the status bar. + /// {@endtemplate} + /// + /// Defaults to true when [scrollDirection] is [Axis.vertical] and + /// [controller] is null. + final bool? primary; + + /// {@template flutter.widgets.scroll_view.physics} + /// How the scroll view should respond to user input. + /// + /// For example, determines how the scroll view continues to animate after the + /// user stops dragging the scroll view. + /// + /// Defaults to matching platform conventions. Furthermore, if [primary] is + /// false, then the user cannot scroll if there is insufficient content to + /// scroll, while if [primary] is true, they can always attempt to scroll. + /// + /// To force the scroll view to always be scrollable even if there is + /// insufficient content, as if [primary] was true but without necessarily + /// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics + /// object, as in: + /// + /// ```dart + /// physics: const AlwaysScrollableScrollPhysics(), + /// ``` + /// + /// To force the scroll view to use the default platform conventions and not + /// be scrollable if there is insufficient content, regardless of the value of + /// [primary], provide an explicit [ScrollPhysics] object, as in: + /// + /// ```dart + /// physics: const ScrollPhysics(), + /// ``` + /// + /// The physics can be changed dynamically (by providing a new object in a + /// subsequent build), but new physics will only take effect if the _class_ of + /// the provided object changes. Merely constructing a new instance with a + /// different configuration is insufficient to cause the physics to be + /// reapplied. (This is because the final object used is generated + /// dynamically, which can be relatively expensive, and it would be + /// inefficient to speculatively create this object each frame to see if the + /// physics should be updated.) + /// {@endtemplate} + /// + /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the + /// [ScrollPhysics] provided by that behavior will take precedence after + /// [physics]. + final ScrollPhysics? physics; + + /// {@template flutter.widgets.scroll_view.shrinkWrap} + /// Whether the extent of the scroll view in the [scrollDirection] should be + /// determined by the contents being viewed. + /// + /// If the scroll view does not shrink wrap, then the scroll view will expand + /// to the maximum allowed size in the [scrollDirection]. If the scroll view + /// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must + /// be true. + /// + /// Shrink wrapping the content of the scroll view is significantly more + /// expensive than expanding to the maximum allowed size because the content + /// can expand and contract during scrolling, which means the size of the + /// scroll view needs to be recomputed whenever the scroll position changes. + /// + /// Defaults to false. + /// {@endtemplate} + final bool shrinkWrap; + + /// The amount of space by which to inset the children. + final EdgeInsetsGeometry? padding; + + /// Whether to wrap each child in an [AutomaticKeepAlive]. + /// + /// Typically, children in lazy list are wrapped in [AutomaticKeepAlive] + /// widgets so that children can use [KeepAliveNotification]s to preserve + /// their state when they would otherwise be garbage collected off-screen. + /// + /// This feature (and [addRepaintBoundaries]) must be disabled if the children + /// are going to manually maintain their [KeepAlive] state. It may also be + /// more efficient to disable this feature if it is known ahead of time that + /// none of the children will ever try to keep themselves alive. + /// + /// Defaults to true. + final bool addAutomaticKeepAlives; + + /// Whether to wrap each child in a [RepaintBoundary]. + /// + /// Typically, children in a scrolling container are wrapped in repaint + /// boundaries so that they do not need to be repainted as the list scrolls. + /// If the children are easy to repaint (e.g., solid color blocks or a short + /// snippet of text), it might be more efficient to not add a repaint boundary + /// and simply repaint the children during scrolling. + /// + /// Defaults to true. + final bool addRepaintBoundaries; + + /// Whether to wrap each child in an [IndexedSemantics]. + /// + /// Typically, children in a scrolling container must be annotated with a + /// semantic index in order to generate the correct accessibility + /// announcements. This should only be set to false if the indexes have + /// already been provided by an [IndexedSemantics] widget. + /// + /// Defaults to true. + /// + /// See also: + /// + /// * [IndexedSemantics], for an explanation of how to manually + /// provide semantic indexes. + final bool addSemanticIndexes; + + /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} + final double? cacheExtent; + + /// The number of children that will contribute semantic information. + /// + /// Some subtypes of [ScrollView] can infer this value automatically. For + /// example [ListView] will use the number of widgets in the child list, + /// while the [ListView.separated] constructor will use half that amount. + /// + /// For [CustomScrollView] and other types which do not receive a builder + /// or list of widgets, the child count must be explicitly provided. If the + /// number is unknown or unbounded this should be left unset or set to null. + /// + /// See also: + /// + /// * [SemanticsConfiguration.scrollChildCount], + /// the corresponding semantics property. + final int? semanticChildCount; + + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + + /// {@template flutter.widgets.scroll_view.keyboardDismissBehavior} + /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will + /// dismiss the keyboard automatically. + /// {@endtemplate} + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + + /// {@macro flutter.widgets.scrollable.restorationId} + final String? restorationId; + + /// {@macro flutter.material.Material.clipBehavior} + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + + /// The thumbnail size. + final ThumbnailSize thumbnailSize; + + /// {@macro photo_manager.ThumbnailFormat} + final ThumbnailFormat thumbnailFormat; + + /// The quality value for the thumbnail. + /// + /// Valid from 1 to 100. + /// Defaults to 100. + final int thumbnailQuality; + + /// Scale of the image. + final double thumbnailScale; + + @override + Widget build(BuildContext context) { + return PagedValueGridView( + scrollDirection: scrollDirection, + reverse: reverse, + controller: controller, + primary: primary, + physics: physics, + shrinkWrap: shrinkWrap, + padding: padding, + scrollController: scrollController, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + cacheExtent: cacheExtent, + semanticChildCount: semanticChildCount, + dragStartBehavior: dragStartBehavior, + keyboardDismissBehavior: keyboardDismissBehavior, + restorationId: restorationId, + clipBehavior: clipBehavior, + loadMoreTriggerIndex: loadMoreTriggerIndex, + gridDelegate: gridDelegate, + itemBuilder: (context, mediaList, index) { + final media = mediaList[index]; + final onTap = onMediaTap; + final onLongPress = onMediaLongPress; + + final streamPhotoGalleryTile = StreamPhotoGalleryTile( + media: media, + onTap: onTap == null ? null : () => onTap(media), + onLongPress: onLongPress == null ? null : () => onLongPress(media), + thumbnailSize: thumbnailSize, + thumbnailFormat: thumbnailFormat, + thumbnailQuality: thumbnailQuality, + thumbnailScale: thumbnailScale, + ); + + return itemBuilder?.call( + context, + mediaList, + index, + streamPhotoGalleryTile, + ) ?? + streamPhotoGalleryTile; + }, + emptyBuilder: (context) { + final chatThemeData = StreamChatTheme.of(context); + return emptyBuilder?.call(context) ?? + Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: StreamScrollViewEmptyWidget( + emptyIcon: StreamSvgIcon.pictures( + size: 148, + color: chatThemeData.colorTheme.disabled, + ), + emptyTitle: Text( + context.translations.noUsersLabel, + style: chatThemeData.textTheme.headline, + ), + ), + ), + ); + }, + loadMoreErrorBuilder: (context, error) { + return StreamScrollViewLoadMoreError.grid( + onTap: controller.retry, + error: Text( + context.translations.loadingUsersError, + textAlign: TextAlign.center, + ), + ); + }, + loadMoreIndicatorBuilder: (context) { + return const Center( + child: Padding( + padding: EdgeInsets.all(16), + child: StreamScrollViewLoadMoreIndicator(), + ), + ); + }, + loadingBuilder: (context) { + return loadingBuilder?.call(context) ?? + const Center( + child: StreamScrollViewLoadingWidget(), + ); + }, + errorBuilder: (context, error) { + return errorBuilder?.call(context, error) ?? + Center( + child: StreamScrollViewErrorWidget( + errorTitle: Text(context.translations.loadingUsersError), + onRetryPressed: controller.refresh, + ), + ); + }, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery_controller.dart b/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery_controller.dart new file mode 100644 index 00000000..eb9b21bb --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery_controller.dart @@ -0,0 +1,87 @@ +import 'package:collection/collection.dart'; +import 'package:photo_manager/photo_manager.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// +class StreamPhotoGalleryController + extends PagedValueNotifier { + /// + StreamPhotoGalleryController({ + this.limit = 50, + }) : super(const PagedValue.loading()); + + /// The maximum number of items to load at once. + final int limit; + + Future _getRecentAssetPathList({ + RequestType type = RequestType.common, + FilterOptionGroup? filterOption, + }) { + return PhotoManager.getAssetPathList( + type: type, + onlyAll: true, + filterOption: filterOption, + ).then((it) => it.firstOrNull); + } + + @override + Future doInitialLoad() async { + try { + final assets = await _getRecentAssetPathList(); + + if (assets == null) { + value = const PagedValue(items: []); + return; + } + + final mediaList = await assets.getAssetListPaged( + page: 0, + size: limit, + ); + + final nextKey = mediaList.length < limit ? null : 1; + value = PagedValue( + items: mediaList, + nextPageKey: nextKey, + ); + } on StreamChatError catch (error) { + value = PagedValue.error(error); + } catch (error) { + final chatError = StreamChatError(error.toString()); + value = PagedValue.error(chatError); + } + } + + @override + Future loadMore(int page) async { + final previousValue = value.asSuccess; + + try { + final assets = await _getRecentAssetPathList(); + + if (assets == null) { + const chatError = StreamChatError('No media found'); + value = previousValue.copyWith(error: chatError); + return; + } + + final mediaList = await assets.getAssetListPaged( + page: page, + size: limit, + ); + + final previousItems = previousValue.items; + final newItems = previousItems + mediaList; + final nextKey = mediaList.length < limit ? null : page + 1; + value = PagedValue( + items: newItems, + nextPageKey: nextKey, + ); + } on StreamChatError catch (error) { + value = previousValue.copyWith(error: error); + } catch (error) { + final chatError = StreamChatError(error.toString()); + value = previousValue.copyWith(error: chatError); + } + } +} diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery_tile.dart b/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery_tile.dart new file mode 100644 index 00000000..e92c3f24 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery_tile.dart @@ -0,0 +1,259 @@ +import 'dart:ui' as ui; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:photo_manager/photo_manager.dart'; +import 'package:stream_chat_flutter/src/misc/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; + +/// Widget that displays a photo or video item from the gallery. +class StreamPhotoGalleryTile extends StatelessWidget { + /// Creates a new instance of [StreamPhotoGalleryTile]. + const StreamPhotoGalleryTile({ + super.key, + required this.media, + this.selected = false, + this.onTap, + this.onLongPress, + this.thumbnailSize = const ThumbnailSize(400, 400), + this.thumbnailFormat = ThumbnailFormat.jpeg, + this.thumbnailQuality = 100, + this.thumbnailScale = 1, + }); + + /// The media item to display. + final AssetEntity media; + + /// Whether the media item is selected. + final bool selected; + + /// Called when the user taps this grid tile. + final GestureTapCallback? onTap; + + /// Called when the user long-presses on this grid tile. + final GestureLongPressCallback? onLongPress; + + /// The thumbnail size. + final ThumbnailSize thumbnailSize; + + /// {@macro photo_manager.ThumbnailFormat} + final ThumbnailFormat thumbnailFormat; + + /// The quality value for the thumbnail. + /// + /// Valid from 1 to 100. + /// Defaults to 100. + final int thumbnailQuality; + + /// Scale of the image. + final double thumbnailScale; + + /// Creates a copy of this tile but with the given fields replaced with + /// the new values. + StreamPhotoGalleryTile copyWith({ + Key? key, + AssetEntity? media, + bool? selected, + GestureTapCallback? onTap, + GestureLongPressCallback? onLongPress, + ThumbnailSize? thumbnailSize, + ThumbnailFormat? thumbnailFormat, + int? thumbnailQuality, + double? thumbnailScale, + }) => + StreamPhotoGalleryTile( + key: key ?? this.key, + media: media ?? this.media, + selected: selected ?? this.selected, + onTap: onTap ?? this.onTap, + onLongPress: onLongPress ?? this.onLongPress, + thumbnailSize: thumbnailSize ?? this.thumbnailSize, + thumbnailFormat: thumbnailFormat ?? this.thumbnailFormat, + thumbnailQuality: thumbnailQuality ?? this.thumbnailQuality, + thumbnailScale: thumbnailScale ?? this.thumbnailScale, + ); + + @override + Widget build(BuildContext context) { + final chatThemeData = StreamChatTheme.of(context); + return Stack( + children: [ + AspectRatio( + aspectRatio: 1, + child: FadeInImage( + fadeInDuration: const Duration(milliseconds: 300), + placeholder: const AssetImage( + 'images/placeholder.png', + package: 'stream_chat_flutter', + ), + fit: BoxFit.cover, + image: MediaThumbnailProvider( + media: media, + size: thumbnailSize, + format: thumbnailFormat, + quality: thumbnailQuality, + scale: thumbnailScale, + ), + ), + ), + Positioned.fill( + child: IgnorePointer( + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: selected ? 1.0 : 0.0, + child: Container( + color: + chatThemeData.colorTheme.textHighEmphasis.withOpacity(0.5), + alignment: Alignment.topRight, + padding: const EdgeInsets.only( + top: 8, + right: 8, + ), + child: CircleAvatar( + radius: 12, + backgroundColor: chatThemeData.colorTheme.barsBg, + child: StreamSvgIcon.check( + size: 24, + color: chatThemeData.colorTheme.textHighEmphasis, + ), + ), + ), + ), + ), + ), + if (media.type == AssetType.video) ...[ + Positioned( + left: 8, + bottom: 10, + child: StreamSvgIcon.videoCall(), + ), + Positioned( + right: 4, + bottom: 10, + child: Text( + media.videoDuration.format(), + style: TextStyle( + color: chatThemeData.colorTheme.barsBg, + ), + ), + ), + ], + // https://stackoverflow.com/a/59317162/10036882 + Positioned.fill( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + onLongPress: onLongPress, + ), + ), + ), + ], + ); + } +} + +extension on Duration { + String format() { + final s = '$this'.split('.')[0].padLeft(8, '0'); + if (s.startsWith('00:')) { + return s.replaceFirst('00:', ''); + } + + return s; + } +} + +/// {@template mediaThumbnailProvider} +/// Builds a thumbnail using [ImageProvider]. +/// {@endtemplate} +class MediaThumbnailProvider extends ImageProvider { + /// {@macro mediaThumbnailProvider} + const MediaThumbnailProvider({ + required this.media, + // TODO: Are these sizes optimal? Consider web/desktop + this.size = const ThumbnailSize(400, 400), + this.format = ThumbnailFormat.jpeg, + this.quality = 100, + this.scale = 2, + }); + + /// Media to load + final AssetEntity media; + + /// The thumbnail size. + final ThumbnailSize size; + + /// {@macro photo_manager.ThumbnailFormat} + final ThumbnailFormat format; + + /// The quality value for the thumbnail. + /// + /// Valid from 1 to 100. + /// Defaults to 100. + final int quality; + + /// Scale of the image. + final double scale; + + @override + Future obtainKey(ImageConfiguration configuration) { + return SynchronousFuture(this); + } + + @override + ImageStreamCompleter loadBuffer( + MediaThumbnailProvider key, + DecoderBufferCallback decode, + ) { + return MultiFrameImageStreamCompleter( + codec: _loadAsync(key, decode), + scale: key.scale, + informationCollector: () sync* { + yield DiagnosticsProperty( + 'Thumbnail provider: $this \n Thumbnail key: $key', + this, + style: DiagnosticsTreeStyle.errorProperty, + ); + }, + ); + } + + Future _loadAsync( + MediaThumbnailProvider key, + DecoderBufferCallback decode, + ) async { + assert(key == this, '$key is not $this'); + final bytes = await media.thumbnailDataWithSize( + size, + format: format, + quality: quality, + ); + final buffer = await ui.ImmutableBuffer.fromUint8List(bytes!); + return decode(buffer); + } + + @override + bool operator ==(dynamic other) { + if (other is MediaThumbnailProvider) { + return media == other.media && + size == other.size && + format == other.format && + quality == other.quality && + scale == other.scale; + } + return false; + } + + @override + int get hashCode => Object.hash(media, size, format, quality, scale); + + @override + String toString() => '$runtimeType(' + 'media: $media, ' + 'size: $size, ' + 'format: $format, ' + 'quality: $quality, ' + 'scale: $scale' + ')'; +} diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_empty_widget.dart b/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_empty_widget.dart similarity index 96% rename from packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_empty_widget.dart rename to packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_empty_widget.dart index f931dbbb..6fe8b852 100644 --- a/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_empty_widget.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_empty_widget.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// A widget that shows an empty view when the [StreamScrollView] loads /// empty data. diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_error_widget.dart b/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_error_widget.dart similarity index 95% rename from packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_error_widget.dart rename to packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_error_widget.dart index bb14d8dd..ccb36403 100644 --- a/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_error_widget.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_error_widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// A widget that is displayed when a [StreamScrollView] encounters an error /// while loading the initial items. diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_indexed_widget_builder.dart b/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_indexed_widget_builder.dart similarity index 100% rename from packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_indexed_widget_builder.dart rename to packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_indexed_widget_builder.dart diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_load_more_error.dart b/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_load_more_error.dart similarity index 96% rename from packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_load_more_error.dart rename to packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_load_more_error.dart index feec6f4e..b2b4d15d 100644 --- a/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_load_more_error.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_load_more_error.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; -import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// A tile that is used to display the error indicator when /// loading more items fails. diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_load_more_indicator.dart b/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_load_more_indicator.dart similarity index 100% rename from packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_load_more_indicator.dart rename to packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_load_more_indicator.dart diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_loading_widget.dart b/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_loading_widget.dart similarity index 100% rename from packages/stream_chat_flutter/lib/src/v4/scroll_view/stream_scroll_view_loading_widget.dart rename to packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_loading_widget.dart diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_grid_tile.dart b/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_grid_tile.dart similarity index 100% rename from packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_grid_tile.dart rename to packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_grid_tile.dart diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_grid_view.dart b/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_grid_view.dart similarity index 97% rename from packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_grid_view.dart rename to packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_grid_view.dart index 13a12100..647061dd 100644 --- a/packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_grid_view.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_grid_view.dart @@ -1,10 +1,10 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_error_widget.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_error.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_indicator.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_loading_widget.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Default grid delegate for [StreamUserGridView]. diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_list_tile.dart b/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_list_tile.dart similarity index 90% rename from packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_list_tile.dart rename to packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_list_tile.dart index 00471855..18942df8 100644 --- a/packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_list_tile.dart @@ -1,11 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:jiffy/jiffy.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; -import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; -import 'package:stream_chat_flutter/src/user_avatar.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart' - show User; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// A widget that displays a user. /// @@ -48,11 +43,11 @@ class StreamUserListTile extends StatelessWidget { /// A widget to display at the end of tile. final Widget? selectedWidget; - /// If this tile is also [enabled] then icons - /// and text are rendered with the same color. + /// If this tile is also [enabled] then icons and text are rendered with the + /// same color. /// - /// By default the selected color is the theme's primary color. - /// The selected color can be overridden with a [ListTileTheme]. + /// By default the selected color is the theme's primary color. The selected + /// color can be overridden with a [ListTileTheme]. /// /// {@tool dartpad} /// Here is an example of using a [StatefulWidget] to keep track of the diff --git a/packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_list_view.dart b/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_list_view.dart similarity index 96% rename from packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_list_view.dart rename to packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_list_view.dart index f54ce795..a0add720 100644 --- a/packages/stream_chat_flutter/lib/src/v4/scroll_view/user_scroll_view/stream_user_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_list_view.dart @@ -2,11 +2,11 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_error_widget.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_error.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_load_more_indicator.dart'; -import 'package:stream_chat_flutter/src/v4/scroll_view/stream_scroll_view_loading_widget.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart'; +import 'package:stream_chat_flutter/src/utils/utils.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Default separator builder for [StreamUserListView]. diff --git a/packages/stream_chat_flutter/lib/src/stream_chat.dart b/packages/stream_chat_flutter/lib/src/stream_chat.dart index cef33020..73a0d0a0 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat.dart @@ -2,8 +2,10 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_portal/flutter_portal.dart'; +import 'package:stream_chat_flutter/src/video/vlc/vlc_manager.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@template streamChat} /// Widget used to provide information about the chat to the widget tree /// /// class MyApp extends StatelessWidget { @@ -25,19 +27,21 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// } /// /// Use [StreamChat.of] to get the current [StreamChatState] instance. +/// {@endtemplate} class StreamChat extends StatefulWidget { - /// Constructor for creating a [StreamChat] widget + /// {@macro streamChat} const StreamChat({ super.key, required this.client, required this.child, this.streamChatThemeData, + this.streamChatConfigData, this.onBackgroundEventReceived, this.backgroundKeepAlive = const Duration(minutes: 1), this.connectivityStream, }); - /// Client to do chat ops with + /// Client to do chat operations with final StreamChatClient client; /// Child which inherits details @@ -46,6 +50,9 @@ class StreamChat extends StatefulWidget { /// Theme to pass on final StreamChatThemeData? streamChatThemeData; + /// Non-theme related UI configuration options. + final StreamChatConfigurationData? streamChatConfigData; + /// The amount of time that will pass before disconnecting the client /// in the background final Duration backgroundKeepAlive; @@ -84,40 +91,57 @@ class StreamChatState extends State { /// Gets client from widget StreamChatClient get client => widget.client; + /// Gets configuration options from widget + StreamChatConfigurationData get streamChatConfigData => + widget.streamChatConfigData ?? StreamChatConfigurationData(); + + @override + void initState() { + super.initState(); + // Ensures that VLC only initializes in real desktop environments + if (!isTestEnvironment && isDesktopVideoPlayerSupported) { + VlcManager.instance.initialize(); + } + } + @override Widget build(BuildContext context) { final theme = _getTheme(context, widget.streamChatThemeData); return Portal( - child: StreamChatTheme( - data: theme, - child: Builder( - builder: (context) { - final materialTheme = Theme.of(context); - final streamTheme = StreamChatTheme.of(context); - return Theme( - data: materialTheme.copyWith( - primaryIconTheme: streamTheme.primaryIconTheme, - colorScheme: materialTheme.colorScheme.copyWith( - secondary: streamTheme.colorTheme.accentPrimary, + child: StreamChatConfiguration( + data: streamChatConfigData, + child: StreamChatTheme( + data: theme, + child: Builder( + builder: (context) { + final materialTheme = Theme.of(context); + final streamTheme = StreamChatTheme.of(context); + return Theme( + data: materialTheme.copyWith( + primaryIconTheme: streamTheme.primaryIconTheme, + colorScheme: materialTheme.colorScheme.copyWith( + secondary: streamTheme.colorTheme.accentPrimary, + ), ), - ), - child: StreamChatCore( - client: client, - onBackgroundEventReceived: widget.onBackgroundEventReceived, - backgroundKeepAlive: widget.backgroundKeepAlive, - connectivityStream: widget.connectivityStream, - child: Builder( - builder: (context) { - StreamChatClient.additionalHeaders = { - 'X-Stream-Client': '${StreamChatClient.defaultUserAgent}-' - 'ui-${StreamChatClient.packageVersion}', - }; - return widget.child ?? const Offstage(); - }, + child: StreamChatCore( + client: client, + onBackgroundEventReceived: widget.onBackgroundEventReceived, + backgroundKeepAlive: widget.backgroundKeepAlive, + connectivityStream: widget.connectivityStream, + child: Builder( + builder: (context) { + StreamChatClient.additionalHeaders = { + 'X-Stream-Client': + '${StreamChatClient.defaultUserAgent}-' + 'ui-${StreamChatClient.packageVersion}', + }; + return widget.child ?? const Offstage(); + }, + ), ), - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart b/packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart new file mode 100644 index 00000000..76e4120f --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart @@ -0,0 +1,229 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template streamChatConfiguration} +/// Inherited widget providing the [StreamChatConfigurationData] +/// to the widget tree +/// {@endtemplate} +class StreamChatConfiguration extends InheritedWidget { + /// {@macro streamChatConfiguration} + const StreamChatConfiguration({ + super.key, + required this.data, + required super.child, + }); + + /// {@macro streamChatConfigurationData} + final StreamChatConfigurationData data; + + @override + bool updateShouldNotify(StreamChatConfiguration oldWidget) => + data != oldWidget.data; + + /// Use this method to get the current [StreamChatThemeData] instance + static StreamChatConfigurationData of(BuildContext context) { + final streamChatConfiguration = + context.dependOnInheritedWidgetOfExactType(); + + assert( + streamChatConfiguration != null, + ''' +You must have a StreamChatConfigurationProvider widget at the top of your widget tree''', + ); + + return streamChatConfiguration!.data; + } +} + +/// {@template streamChatConfigurationData} +/// Provides global, user-configurable, non-theme related configuration +/// options to Flutter applications that use Stream Chat. +/// +/// In order to set these configuration options, you must pass an instance of +/// this class to the [StreamChat] widget, or wrap a subtree using +/// the [StreamChatConfiguration] inherited widget. +/// +/// If you need to access the configuration directly at a later point in your +/// application, you can use the [StreamChatConfiguration.of] method +/// to retrieve it. +/// +/// If no [StreamChatConfigurationData] is provided, the +/// [StreamChatConfiguration.defaults] factory constructor is used to provide a +/// default configuration. +/// +/// If you want to keep some of the default values, but not others, you can use +/// the [StreamChatConfigurationData.copyWith] method to override the values in +/// question. +/// +/// Example 1: +/// ```dart +/// class MyApp extends StatelessWidget { +/// const MyApp({ +/// required this.client, +/// }); +/// +/// final StreamChatClient client; +/// +/// @override +/// Widget build(BuildContext context) { +/// return MaterialApp( +/// home: Container( +/// child: StreamChat( +/// client: client, +/// // No configuration provided, so the defaults are used. +/// child: ChannelListPage(), +/// ), +/// ), +/// ); +/// } +/// } +/// ``` +/// +/// Example 2: +/// ```dart +/// class MyApp extends StatelessWidget { +/// const MyApp({ +/// required this.client, +/// }); +/// +/// final StreamChatClient client; +/// +/// @override +/// Widget build(BuildContext context) { +/// return MaterialApp( +/// home: Container( +/// child: StreamChat( +/// client: client, +/// config: StreamChatConfiguration.defaults().copyWith( +/// // Override a specific default value here +/// ), +/// child: ChannelListPage(), +/// ), +/// ), +/// ); +/// } +/// } +/// ``` +/// {@endtemplate} +class StreamChatConfigurationData { + /// {@macro streamChatConfigurationData} + factory StreamChatConfigurationData({ + Widget Function(BuildContext, User)? defaultUserImage, + Widget Function(BuildContext, User)? placeholderUserImage, + List? reactionIcons, + bool? enforceUniqueReactions, + }) { + return StreamChatConfigurationData._( + defaultUserImage: defaultUserImage ?? _defaultUserImage, + placeholderUserImage: placeholderUserImage, + reactionIcons: reactionIcons ?? _defaultReactionIcons, + enforceUniqueReactions: enforceUniqueReactions ?? true, + ); + } + + StreamChatConfigurationData._({ + required this.defaultUserImage, + required this.placeholderUserImage, + required this.reactionIcons, + required this.enforceUniqueReactions, + }); + + /// Copies the configuration options from one [StreamChatConfigurationData] to + /// another. + StreamChatConfigurationData copyWith({ + Widget Function(BuildContext, User)? defaultUserImage, + Widget Function(BuildContext, User)? placeholderUserImage, + List? reactionIcons, + bool? enforceUniqueReactions, + }) { + return StreamChatConfigurationData( + defaultUserImage: defaultUserImage ?? this.defaultUserImage, + placeholderUserImage: placeholderUserImage ?? this.placeholderUserImage, + reactionIcons: reactionIcons ?? this.reactionIcons, + enforceUniqueReactions: + enforceUniqueReactions ?? this.enforceUniqueReactions, + ); + } + + /// The widget that will be built when the user image is unavailable. + final Widget Function(BuildContext, User) defaultUserImage; + + /// The widget that will be built when the user image is loading. + final Widget Function(BuildContext, User)? placeholderUserImage; + + /// Assets used for rendering reactions. + final List reactionIcons; + + /// Whether a new reaction should replace the existing one. + final bool enforceUniqueReactions; + + static final _defaultReactionIcons = [ + StreamReactionIcon( + type: 'love', + builder: (context, highlighted, size) { + final theme = StreamChatTheme.of(context); + return StreamSvgIcon.loveReaction( + color: highlighted + ? theme.colorTheme.accentPrimary + : theme.primaryIconTheme.color!.withOpacity(0.5), + size: size, + ); + }, + ), + StreamReactionIcon( + type: 'like', + builder: (context, highlighted, size) { + final theme = StreamChatTheme.of(context); + return StreamSvgIcon.thumbsUpReaction( + color: highlighted + ? theme.colorTheme.accentPrimary + : theme.primaryIconTheme.color!.withOpacity(0.5), + size: size, + ); + }, + ), + StreamReactionIcon( + type: 'sad', + builder: (context, highlighted, size) { + final theme = StreamChatTheme.of(context); + return StreamSvgIcon.thumbsDownReaction( + color: highlighted + ? theme.colorTheme.accentPrimary + : theme.primaryIconTheme.color!.withOpacity(0.5), + size: size, + ); + }, + ), + StreamReactionIcon( + type: 'haha', + builder: (context, highlighted, size) { + final theme = StreamChatTheme.of(context); + return StreamSvgIcon.lolReaction( + color: highlighted + ? theme.colorTheme.accentPrimary + : theme.primaryIconTheme.color!.withOpacity(0.5), + size: size, + ); + }, + ), + StreamReactionIcon( + type: 'wow', + builder: (context, highlighted, size) { + final theme = StreamChatTheme.of(context); + return StreamSvgIcon.wutReaction( + color: highlighted + ? theme.colorTheme.accentPrimary + : theme.primaryIconTheme.color!.withOpacity(0.5), + size: size, + ); + }, + ), + ]; + + static Widget _defaultUserImage(BuildContext context, User user) => Center( + child: StreamGradientAvatar( + name: user.name, + userId: user.id, + ), + ); +} diff --git a/packages/stream_chat_flutter/lib/src/stream_neumorphic_button.dart b/packages/stream_chat_flutter/lib/src/stream_neumorphic_button.dart deleted file mode 100644 index b967f7e4..00000000 --- a/packages/stream_chat_flutter/lib/src/stream_neumorphic_button.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flutter/material.dart'; - -/// Neumorphic button -class StreamNeumorphicButton extends StatelessWidget { - /// Constructor for creating [StreamNeumorphicButton] - const StreamNeumorphicButton({ - super.key, - required this.child, - this.backgroundColor = Colors.white, - }); - - /// Child contained in the button - final Widget child; - - /// Background color of button - final Color backgroundColor; - - @override - Widget build(BuildContext context) => Container( - margin: const EdgeInsets.all(8), - height: 40, - width: 40, - decoration: BoxDecoration( - color: backgroundColor, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.grey.shade700, - offset: const Offset(0, 1), - blurRadius: 0.5, - ), - const BoxShadow( - color: Colors.white, - blurRadius: 0.5, - ), - ], - ), - child: child, - ); -} diff --git a/packages/stream_chat_flutter/lib/src/stream_svg_icon.dart b/packages/stream_chat_flutter/lib/src/stream_svg_icon.dart deleted file mode 100644 index efe5a33e..00000000 --- a/packages/stream_chat_flutter/lib/src/stream_svg_icon.dart +++ /dev/null @@ -1,999 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; - -/// Icon set of stream chat -class StreamSvgIcon extends StatelessWidget { - /// Constructor for creating a [StreamSvgIcon] - const StreamSvgIcon({ - super.key, - this.assetName, - this.color, - this.width = 24, - this.height = 24, - }); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.settings({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'settings.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.down({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_down.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.up({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_up.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.attach({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_attach.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.loveReaction({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_love_reaction.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.thumbsUpReaction({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_thumbs_up_reaction.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.thumbsDownReaction({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_thumbs_down_reaction.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.lolReaction({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_LOL_reaction.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.wutReaction({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_wut_reaction.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.smile({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_smile.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.mentions({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'mentions.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.record({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_record.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.camera({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_camera.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.files({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'files.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.pictures({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'pictures.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.left({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_left.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.user({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_user.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.userAdd({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_User_add.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.check({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_check.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.checkAll({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_check_all.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.checkSend({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_check_send.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.penWrite({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_pen-write.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.contacts({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_contacts.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.close({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_close.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.search({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_search.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.right({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_right.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.mute({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_mute.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.userRemove({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_User_deselect.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.lightning({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_lightning-command runner.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.emptyCircleLeft({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_empty_circle_left.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.message({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_message.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.thread({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_Thread_Reply.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.reply({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_curve_line_left_up_big.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.edit({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_edit.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.download({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_download.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.cloudDownload({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_cloud_download.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.copy({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_copy.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.delete({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_delete.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.eye({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_eye-off.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.arrowRight({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_arrow_right.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.closeSmall({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_close_sml.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.iconCurveLineLeftUp({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_curve_line_left_up.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.iconMoon({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'icon_moon.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.iconShare({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'icon_SHARE.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.iconGrid({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_grid.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.iconSendMessage({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_send_message.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.iconMenuPoint({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_menu_point_v.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.iconSave({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_save.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.shareArrow({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'share_arrow.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.filetype7z({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'filetype_7z.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.filetypeCsv({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'filetype_CSV.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.filetypeDoc({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'filetype_DOC.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.filetypeDocx({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'filetype_DOCX.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.filetypeGeneric({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'filetype_Generic.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.filetypeHtml({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'filetype_html.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.filetypeMd({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'filetype_MD.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.filetypeOdt({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'filetype_ODT.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.filetypePdf({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'filetype_PDF.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.filetypePpt({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'filetype_PPT.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.filetypePptx({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'filetype_PPTX.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.filetypeRar({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'filetype_RAR.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.filetypeRtf({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'filetype_RTF.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.filetypeTar({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'filetype_TAR.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.filetypeTxt({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'filetype_TXT.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.filetypeXls({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'filetype_XLS.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.filetypeXlsx({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'filetype_XLSX.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.filetypeZip({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'filetype_ZIP.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.iconGroup({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_group.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.iconNotification({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_notification.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.iconUserDelete({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_user_delete.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.error({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_error.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.circleUp({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_circle_up.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.iconUserSettings({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'Icon_user_settings.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.giphyIcon({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'giphy_icon.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.imgur({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'imgur.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.volumeUp({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'volume-up.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.flag({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'flag.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.iconFlag({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'icon_flag.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.retry({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'icon_retry.svg', - color: color, - width: size, - height: size, - ); - - /// [StreamSvgIcon] type - factory StreamSvgIcon.pin({ - double? size, - Color? color, - }) => - StreamSvgIcon( - assetName: 'icon_pin.svg', - color: color, - width: size, - height: size, - ); - - /// Name of icon asset - final String? assetName; - - /// Width of icon - final double? width; - - /// Height of icon - final double? height; - - /// Color of icon - final Color? color; - - @override - Widget build(BuildContext context) { - final key = Key('StreamSvgIcon-$assetName'); - return SvgPicture.asset( - 'lib/svgs/$assetName', - package: 'stream_chat_flutter', - key: key, - width: width, - height: height, - color: color, - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart b/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart index eb2d79bf..dbc36da9 100644 --- a/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart @@ -1,16 +1,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -/// {@macro avatar_theme_data} -@Deprecated("Use 'StreamAvatarThemeData' instead") -typedef AvatarThemeData = StreamAvatarThemeData; - -/// {@template avatar_theme_data} +/// {@template avatarThemeData} /// A style that overrides the default appearance of various avatar widgets. /// {@endtemplate} // ignore: prefer-match-file-name class StreamAvatarThemeData with Diagnosticable { - /// Creates an [StreamAvatarThemeData]. + /// {@macro avatarThemeData} const StreamAvatarThemeData({ BoxConstraints? constraints, BorderRadius? borderRadius, @@ -35,11 +31,12 @@ class StreamAvatarThemeData with Diagnosticable { StreamAvatarThemeData copyWith({ BoxConstraints? constraints, BorderRadius? borderRadius, - }) => - StreamAvatarThemeData( - constraints: constraints ?? _constraints, - borderRadius: borderRadius ?? _borderRadius, - ); + }) { + return StreamAvatarThemeData( + constraints: constraints ?? _constraints, + borderRadius: borderRadius ?? _borderRadius, + ); + } /// Linearly interpolate between two [UserAvatar] themes. /// @@ -48,11 +45,12 @@ class StreamAvatarThemeData with Diagnosticable { StreamAvatarThemeData a, StreamAvatarThemeData b, double t, - ) => - StreamAvatarThemeData( - borderRadius: BorderRadius.lerp(a.borderRadius, b.borderRadius, t), - constraints: BoxConstraints.lerp(a.constraints, b.constraints, t), - ); + ) { + return StreamAvatarThemeData( + borderRadius: BorderRadius.lerp(a.borderRadius, b.borderRadius, t), + constraints: BoxConstraints.lerp(a.constraints, b.constraints, t), + ); + } @override bool operator ==(Object other) => diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart index 8a7b6d11..2df24f68 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart @@ -1,13 +1,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/theme/themes.dart'; -/// {@macro channel_header_theme} -@Deprecated("Use 'StreamChannelHeaderTheme' instead") -typedef ChannelHeaderTheme = StreamChannelHeaderTheme; - /// {@template channel_header_theme} /// Overrides the default style of [ChannelHeader] descendants. /// @@ -54,10 +50,6 @@ class StreamChannelHeaderTheme extends InheritedTheme { data != oldWidget.data; } -/// {@macro channel_header_theme_data} -@Deprecated("Use 'StreamChannelHeaderThemeData' instead") -typedef ChannelHeaderThemeData = StreamChannelHeaderThemeData; - /// {@template channel_header_theme_data} /// A style that overrides the default appearance of [ChannelHeader]s when used /// with [StreamChannelHeaderTheme] or with the overall [StreamChatTheme]'s @@ -96,13 +88,14 @@ class StreamChannelHeaderThemeData with Diagnosticable { TextStyle? subtitleStyle, StreamAvatarThemeData? avatarTheme, Color? color, - }) => - StreamChannelHeaderThemeData( - titleStyle: titleStyle ?? this.titleStyle, - subtitleStyle: subtitleStyle ?? this.subtitleStyle, - avatarTheme: avatarTheme ?? this.avatarTheme, - color: color ?? this.color, - ); + }) { + return StreamChannelHeaderThemeData( + titleStyle: titleStyle ?? this.titleStyle, + subtitleStyle: subtitleStyle ?? this.subtitleStyle, + avatarTheme: avatarTheme ?? this.avatarTheme, + color: color ?? this.color, + ); + } /// Linearly interpolate between two [StreamChannelHeaderThemeData]. /// @@ -111,14 +104,15 @@ class StreamChannelHeaderThemeData with Diagnosticable { StreamChannelHeaderThemeData a, StreamChannelHeaderThemeData b, double t, - ) => - StreamChannelHeaderThemeData( - titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t), - subtitleStyle: TextStyle.lerp(a.subtitleStyle, b.subtitleStyle, t), - avatarTheme: const StreamAvatarThemeData() - .lerp(a.avatarTheme!, b.avatarTheme!, t), - color: Color.lerp(a.color, b.color, t), - ); + ) { + return StreamChannelHeaderThemeData( + titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t), + subtitleStyle: TextStyle.lerp(a.subtitleStyle, b.subtitleStyle, t), + avatarTheme: + const StreamAvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), + color: Color.lerp(a.color, b.color, t), + ); + } /// Merge with other [StreamChannelHeaderThemeData] StreamChannelHeaderThemeData merge(StreamChannelHeaderThemeData? other) { diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart index 265b2626..7452065d 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart @@ -1,13 +1,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; -/// {@macro channel_list_header_theme} -@Deprecated("Use 'StreamChannelListHeaderTheme' instead") -typedef ChannelListHeaderTheme = StreamChannelListHeaderTheme; - -/// {@template channel_list_header_theme} +/// {@template channelListHeaderTheme} /// Overrides the default style of [ChannelListHeader] descendants. /// /// See also: @@ -54,10 +50,6 @@ class StreamChannelListHeaderTheme extends InheritedTheme { data != oldWidget.data; } -/// {@macro channel_list_header_theme_data} -@Deprecated("Use ''StreamChannelListHeaderThemeData' instead") -typedef ChannelListHeaderThemeData = StreamChannelListHeaderThemeData; - /// {@template channel_list_header_theme_data} /// Theme dedicated to the [ChannelListHeader] /// {@endtemplate} @@ -84,12 +76,13 @@ class StreamChannelListHeaderThemeData with Diagnosticable { TextStyle? titleStyle, StreamAvatarThemeData? avatarTheme, Color? color, - }) => - StreamChannelListHeaderThemeData( - titleStyle: titleStyle ?? this.titleStyle, - avatarTheme: avatarTheme ?? this.avatarTheme, - color: color ?? this.color, - ); + }) { + return StreamChannelListHeaderThemeData( + titleStyle: titleStyle ?? this.titleStyle, + avatarTheme: avatarTheme ?? this.avatarTheme, + color: color ?? this.color, + ); + } /// Linearly interpolate from one [StreamChannelListHeaderThemeData] /// to another. @@ -97,13 +90,14 @@ class StreamChannelListHeaderThemeData with Diagnosticable { StreamChannelListHeaderThemeData a, StreamChannelListHeaderThemeData b, double t, - ) => - StreamChannelListHeaderThemeData( - avatarTheme: const StreamAvatarThemeData() - .lerp(a.avatarTheme!, b.avatarTheme!, t), - color: Color.lerp(a.color, b.color, t), - titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t), - ); + ) { + return StreamChannelListHeaderThemeData( + avatarTheme: + const StreamAvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), + color: Color.lerp(a.color, b.color, t), + titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t), + ); + } /// Merges [this] [StreamChannelListHeaderThemeData] with the [other] StreamChannelListHeaderThemeData merge( diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_list_view_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_list_view_theme.dart deleted file mode 100644 index 683f1d0d..00000000 --- a/packages/stream_chat_flutter/lib/src/theme/channel_list_view_theme.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; - -/// {@macro channel_list_view_theme} -@Deprecated("Use 'StreamChannelListViewTheme' instead") -typedef ChannelListViewTheme = StreamChannelListViewTheme; - -/// {@template channel_list_view_theme} -/// Overrides the default style of [ChannelListView] descendants. -/// -/// See also: -/// -/// * [StreamChannelListViewThemeData], which is used to configure this theme. -/// {@endtemplate} -class StreamChannelListViewTheme extends InheritedTheme { - /// Creates a [StreamChannelListViewTheme]. - /// - /// The [data] parameter must not be null. - const StreamChannelListViewTheme({ - super.key, - required this.data, - required super.child, - }); - - /// The configuration of this theme. - final StreamChannelListViewThemeData data; - - /// The closest instance of this class that encloses the given context. - /// - /// If there is no enclosing [StreamChannelListViewTheme] widget, then - /// [StreamChatThemeData.channelListViewTheme] is used. - /// - /// Typical usage is as follows: - /// - /// ```dart - /// ChannelListViewTheme theme = ChannelListViewTheme.of(context); - /// ``` - static StreamChannelListViewThemeData of(BuildContext context) { - final channelListViewTheme = context - .dependOnInheritedWidgetOfExactType(); - return channelListViewTheme?.data ?? - StreamChatTheme.of(context).channelListViewTheme; - } - - @override - Widget wrap(BuildContext context, Widget child) => - StreamChannelListViewTheme(data: data, child: child); - - @override - bool updateShouldNotify(StreamChannelListViewTheme oldWidget) => - data != oldWidget.data; -} - -/// {@macro channel_list_view_theme_data} -@Deprecated("Use 'StreamChannelListViewThemeData' instead") -typedef ChannelListViewThemeData = StreamChannelListViewThemeData; - -/// {@template channel_list_view_theme_data} -/// A style that overrides the default appearance of [ChannelListView]s when -/// used with [StreamChannelListViewTheme] -/// or with the overall [StreamChatTheme]'s -/// [StreamChatThemeData.channelListViewTheme]. -/// -/// See also: -/// -/// * [StreamChannelListViewTheme], the theme -/// which is configured with this class. -/// * [StreamChatThemeData.channelListViewTheme], which can be used to override -/// the default style for [ChannelListView]s below the overall -/// [StreamChatTheme]. -/// {@endtemplate} -class StreamChannelListViewThemeData with Diagnosticable { - /// Creates a [StreamChannelListViewThemeData]. - const StreamChannelListViewThemeData({ - this.backgroundColor, - }); - - /// The color of the [ChannelListView] background. - final Color? backgroundColor; - - /// Copies this [StreamChannelListViewThemeData] to another. - StreamChannelListViewThemeData copyWith({ - Color? backgroundColor, - }) => - StreamChannelListViewThemeData( - backgroundColor: backgroundColor ?? this.backgroundColor, - ); - - /// Linearly interpolate between two [StreamChannelListViewThemeData] themes. - /// - /// All the properties must be non-null. - StreamChannelListViewThemeData lerp( - StreamChannelListViewThemeData a, - StreamChannelListViewThemeData b, - double t, - ) => - StreamChannelListViewThemeData( - backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), - ); - - /// Merges one [StreamChannelListViewThemeData] with another. - StreamChannelListViewThemeData merge(StreamChannelListViewThemeData? other) { - if (other == null) return this; - return copyWith( - backgroundColor: other.backgroundColor, - ); - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is StreamChannelListViewThemeData && - runtimeType == other.runtimeType && - backgroundColor == other.backgroundColor; - - @override - int get hashCode => backgroundColor.hashCode; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(ColorProperty('backgroundColor', backgroundColor)); - } -} diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart index 0a041b67..ff7e5dea 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart @@ -1,13 +1,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; -/// {@macro channel_preview_theme} -@Deprecated("Use 'StreamChannelPreviewTheme' instead") -typedef ChannelPreviewTheme = StreamChannelPreviewTheme; - -/// {@template channel_preview_theme} +/// {@template channelPreviewTheme} /// Overrides the default style of [ChannelPreview] descendants. /// /// See also: @@ -53,11 +49,7 @@ class StreamChannelPreviewTheme extends InheritedTheme { data != oldWidget.data; } -/// {@macro channel_preview_theme_data} -@Deprecated("Use 'StreamChannelPreviewThemeData' instead") -typedef ChannelPreviewThemeData = StreamChannelPreviewThemeData; - -/// {@template channel_preview_theme_data} +/// {@template channelPreviewThemeData} /// A style that overrides the default appearance of [ChannelPreview]s when used /// with [StreamChannelPreviewTheme] or with the overall [StreamChatTheme]'s /// [StreamChatThemeData.channelPreviewTheme]. @@ -106,33 +98,35 @@ class StreamChannelPreviewThemeData with Diagnosticable { StreamAvatarThemeData? avatarTheme, Color? unreadCounterColor, double? indicatorIconSize, - }) => - StreamChannelPreviewThemeData( - titleStyle: titleStyle ?? this.titleStyle, - subtitleStyle: subtitleStyle ?? this.subtitleStyle, - lastMessageAtStyle: lastMessageAtStyle ?? this.lastMessageAtStyle, - avatarTheme: avatarTheme ?? this.avatarTheme, - unreadCounterColor: unreadCounterColor ?? this.unreadCounterColor, - indicatorIconSize: indicatorIconSize ?? this.indicatorIconSize, - ); + }) { + return StreamChannelPreviewThemeData( + titleStyle: titleStyle ?? this.titleStyle, + subtitleStyle: subtitleStyle ?? this.subtitleStyle, + lastMessageAtStyle: lastMessageAtStyle ?? this.lastMessageAtStyle, + avatarTheme: avatarTheme ?? this.avatarTheme, + unreadCounterColor: unreadCounterColor ?? this.unreadCounterColor, + indicatorIconSize: indicatorIconSize ?? this.indicatorIconSize, + ); + } /// Linearly interpolate one [StreamChannelPreviewThemeData] to another. StreamChannelPreviewThemeData lerp( StreamChannelPreviewThemeData a, StreamChannelPreviewThemeData b, double t, - ) => - StreamChannelPreviewThemeData( - avatarTheme: const StreamAvatarThemeData() - .lerp(a.avatarTheme!, b.avatarTheme!, t), - indicatorIconSize: a.indicatorIconSize, - lastMessageAtStyle: - TextStyle.lerp(a.lastMessageAtStyle, b.lastMessageAtStyle, t), - subtitleStyle: TextStyle.lerp(a.subtitleStyle, b.subtitleStyle, t), - titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t), - unreadCounterColor: - Color.lerp(a.unreadCounterColor, b.unreadCounterColor, t), - ); + ) { + return StreamChannelPreviewThemeData( + avatarTheme: + const StreamAvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), + indicatorIconSize: a.indicatorIconSize, + lastMessageAtStyle: + TextStyle.lerp(a.lastMessageAtStyle, b.lastMessageAtStyle, t), + subtitleStyle: TextStyle.lerp(a.subtitleStyle, b.subtitleStyle, t), + titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t), + unreadCounterColor: + Color.lerp(a.unreadCounterColor, b.unreadCounterColor, t), + ); + } /// Merge with theme StreamChannelPreviewThemeData merge(StreamChannelPreviewThemeData? other) { diff --git a/packages/stream_chat_flutter/lib/src/theme/color_theme.dart b/packages/stream_chat_flutter/lib/src/theme/color_theme.dart index e0068d59..0babd24c 100644 --- a/packages/stream_chat_flutter/lib/src/theme/color_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/color_theme.dart @@ -1,9 +1,5 @@ import 'package:flutter/material.dart'; -/// {@macro color_theme} -@Deprecated("Use 'StreamColorTheme' instead") -typedef ColorTheme = StreamColorTheme; - /// {@template color_theme} /// Theme that holds colors /// {@endtemplate} @@ -196,50 +192,51 @@ class StreamColorTheme { Color? overlay, Color? overlayDark, Gradient? bgGradient, - }) => - brightness == Brightness.light - ? StreamColorTheme.light( - textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis, - textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis, - disabled: disabled ?? this.disabled, - borders: borders ?? this.borders, - inputBg: inputBg ?? this.inputBg, - appBg: appBg ?? this.appBg, - barsBg: barsBg ?? this.barsBg, - linkBg: linkBg ?? this.linkBg, - accentPrimary: accentPrimary ?? this.accentPrimary, - accentError: accentError ?? this.accentError, - accentInfo: accentInfo ?? this.accentInfo, - borderTop: borderTop ?? this.borderTop, - borderBottom: borderBottom ?? this.borderBottom, - shadowIconButton: shadowIconButton ?? this.shadowIconButton, - modalShadow: modalShadow ?? this.modalShadow, - highlight: highlight ?? this.highlight, - overlay: overlay ?? this.overlay, - overlayDark: overlayDark ?? this.overlayDark, - bgGradient: bgGradient ?? this.bgGradient, - ) - : StreamColorTheme.dark( - textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis, - textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis, - disabled: disabled ?? this.disabled, - borders: borders ?? this.borders, - inputBg: inputBg ?? this.inputBg, - appBg: appBg ?? this.appBg, - barsBg: barsBg ?? this.barsBg, - linkBg: linkBg ?? this.linkBg, - accentPrimary: accentPrimary ?? this.accentPrimary, - accentError: accentError ?? this.accentError, - accentInfo: accentInfo ?? this.accentInfo, - borderTop: borderTop ?? this.borderTop, - borderBottom: borderBottom ?? this.borderBottom, - shadowIconButton: shadowIconButton ?? this.shadowIconButton, - modalShadow: modalShadow ?? this.modalShadow, - highlight: highlight ?? this.highlight, - overlay: overlay ?? this.overlay, - overlayDark: overlayDark ?? this.overlayDark, - bgGradient: bgGradient ?? this.bgGradient, - ); + }) { + return brightness == Brightness.light + ? StreamColorTheme.light( + textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis, + textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis, + disabled: disabled ?? this.disabled, + borders: borders ?? this.borders, + inputBg: inputBg ?? this.inputBg, + appBg: appBg ?? this.appBg, + barsBg: barsBg ?? this.barsBg, + linkBg: linkBg ?? this.linkBg, + accentPrimary: accentPrimary ?? this.accentPrimary, + accentError: accentError ?? this.accentError, + accentInfo: accentInfo ?? this.accentInfo, + borderTop: borderTop ?? this.borderTop, + borderBottom: borderBottom ?? this.borderBottom, + shadowIconButton: shadowIconButton ?? this.shadowIconButton, + modalShadow: modalShadow ?? this.modalShadow, + highlight: highlight ?? this.highlight, + overlay: overlay ?? this.overlay, + overlayDark: overlayDark ?? this.overlayDark, + bgGradient: bgGradient ?? this.bgGradient, + ) + : StreamColorTheme.dark( + textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis, + textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis, + disabled: disabled ?? this.disabled, + borders: borders ?? this.borders, + inputBg: inputBg ?? this.inputBg, + appBg: appBg ?? this.appBg, + barsBg: barsBg ?? this.barsBg, + linkBg: linkBg ?? this.linkBg, + accentPrimary: accentPrimary ?? this.accentPrimary, + accentError: accentError ?? this.accentError, + accentInfo: accentInfo ?? this.accentInfo, + borderTop: borderTop ?? this.borderTop, + borderBottom: borderBottom ?? this.borderBottom, + shadowIconButton: shadowIconButton ?? this.shadowIconButton, + modalShadow: modalShadow ?? this.modalShadow, + highlight: highlight ?? this.highlight, + overlay: overlay ?? this.overlay, + overlayDark: overlayDark ?? this.overlayDark, + bgGradient: bgGradient ?? this.bgGradient, + ); + } /// Merge color theme StreamColorTheme merge(StreamColorTheme? other) { @@ -301,12 +298,13 @@ class Effect { Color? color, double? alpha, double? blur, - }) => - Effect( - sigmaX: sigmaX ?? this.sigmaX, - sigmaY: sigmaY ?? this.sigmaY, - color: color ?? this.color, - alpha: color as double? ?? this.alpha, - blur: blur ?? this.blur, - ); + }) { + return Effect( + sigmaX: sigmaX ?? this.sigmaX, + sigmaY: sigmaY ?? this.sigmaY, + color: color ?? this.color, + alpha: color as double? ?? this.alpha, + blur: blur ?? this.blur, + ); + } } diff --git a/packages/stream_chat_flutter/lib/src/theme/gallery_footer_theme.dart b/packages/stream_chat_flutter/lib/src/theme/gallery_footer_theme.dart index f23bb317..84e1688a 100644 --- a/packages/stream_chat_flutter/lib/src/theme/gallery_footer_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/gallery_footer_theme.dart @@ -1,12 +1,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; -/// {@macro gallery_footer_theme} -@Deprecated("Use 'StreamGalleryFooterTheme' instead") -typedef GalleryFooterTheme = StreamGalleryFooterTheme; - -/// {@template gallery_footer_theme} +/// {@template galleryFooterTheme} /// Overrides the default style of [GalleryFooter] descendants. /// /// See also: @@ -52,11 +48,7 @@ class StreamGalleryFooterTheme extends InheritedTheme { data != oldWidget.data; } -/// {@macro gallery_footer_theme_data} -@Deprecated("Use 'StreamGalleryFooterThemeData' instead") -typedef GalleryFooterThemeData = StreamGalleryFooterThemeData; - -/// {@template gallery_footer_theme_data} +/// {@template galleryFooterThemeData} /// A style that overrides the default appearance of [GalleryFooter]s when used /// with [StreamGalleryFooterTheme] or with the overall [StreamChatTheme]'s /// [StreamChatThemeData.galleryFooterTheme]. @@ -130,21 +122,22 @@ class StreamGalleryFooterThemeData with Diagnosticable { Color? bottomSheetBackgroundColor, TextStyle? bottomSheetPhotosTextStyle, Color? bottomSheetCloseIconColor, - }) => - StreamGalleryFooterThemeData( - backgroundColor: backgroundColor ?? this.backgroundColor, - shareIconColor: shareIconColor ?? this.shareIconColor, - titleTextStyle: titleTextStyle ?? this.titleTextStyle, - gridIconButtonColor: gridIconButtonColor ?? this.gridIconButtonColor, - bottomSheetBarrierColor: - bottomSheetBarrierColor ?? this.bottomSheetBarrierColor, - bottomSheetBackgroundColor: - bottomSheetBackgroundColor ?? this.bottomSheetBackgroundColor, - bottomSheetPhotosTextStyle: - bottomSheetPhotosTextStyle ?? this.bottomSheetPhotosTextStyle, - bottomSheetCloseIconColor: - bottomSheetCloseIconColor ?? this.bottomSheetCloseIconColor, - ); + }) { + return StreamGalleryFooterThemeData( + backgroundColor: backgroundColor ?? this.backgroundColor, + shareIconColor: shareIconColor ?? this.shareIconColor, + titleTextStyle: titleTextStyle ?? this.titleTextStyle, + gridIconButtonColor: gridIconButtonColor ?? this.gridIconButtonColor, + bottomSheetBarrierColor: + bottomSheetBarrierColor ?? this.bottomSheetBarrierColor, + bottomSheetBackgroundColor: + bottomSheetBackgroundColor ?? this.bottomSheetBackgroundColor, + bottomSheetPhotosTextStyle: + bottomSheetPhotosTextStyle ?? this.bottomSheetPhotosTextStyle, + bottomSheetCloseIconColor: + bottomSheetCloseIconColor ?? this.bottomSheetCloseIconColor, + ); + } /// Linearly interpolate between two [GalleryFooter] themes. /// @@ -153,31 +146,32 @@ class StreamGalleryFooterThemeData with Diagnosticable { StreamGalleryFooterThemeData a, StreamGalleryFooterThemeData b, double t, - ) => - StreamGalleryFooterThemeData( - backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), - shareIconColor: Color.lerp(a.shareIconColor, b.shareIconColor, t), - titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t), - gridIconButtonColor: - Color.lerp(a.gridIconButtonColor, b.gridIconButtonColor, t), - bottomSheetBarrierColor: - Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t), - bottomSheetBackgroundColor: Color.lerp( - a.bottomSheetBackgroundColor, - b.bottomSheetBackgroundColor, - t, - ), - bottomSheetPhotosTextStyle: TextStyle.lerp( - a.bottomSheetPhotosTextStyle, - b.bottomSheetPhotosTextStyle, - t, - ), - bottomSheetCloseIconColor: Color.lerp( - a.bottomSheetCloseIconColor, - b.bottomSheetCloseIconColor, - t, - ), - ); + ) { + return StreamGalleryFooterThemeData( + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + shareIconColor: Color.lerp(a.shareIconColor, b.shareIconColor, t), + titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t), + gridIconButtonColor: + Color.lerp(a.gridIconButtonColor, b.gridIconButtonColor, t), + bottomSheetBarrierColor: + Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t), + bottomSheetBackgroundColor: Color.lerp( + a.bottomSheetBackgroundColor, + b.bottomSheetBackgroundColor, + t, + ), + bottomSheetPhotosTextStyle: TextStyle.lerp( + a.bottomSheetPhotosTextStyle, + b.bottomSheetPhotosTextStyle, + t, + ), + bottomSheetCloseIconColor: Color.lerp( + a.bottomSheetCloseIconColor, + b.bottomSheetCloseIconColor, + t, + ), + ); + } /// Merges one [StreamGalleryFooterThemeData] with another. StreamGalleryFooterThemeData merge(StreamGalleryFooterThemeData? other) { diff --git a/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart index c190ebfe..90977d4c 100644 --- a/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart @@ -1,12 +1,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; -/// {@macro gallery_header_them} -@Deprecated("Use 'StreamGalleryHeaderTheme' instead") -typedef GalleryHeaderTheme = StreamGalleryHeaderTheme; - -/// {@template gallery_header_theme} +/// {@template galleryHeaderTheme} /// Overrides the default style of [GalleryHeader] descendants. /// /// See also: @@ -52,11 +48,7 @@ class StreamGalleryHeaderTheme extends InheritedTheme { data != oldWidget.data; } -/// {@macro gallery_header_theme_data} -@Deprecated("Use 'StreamGalleryHeaderThemeData' instead") -typedef GalleryHeaderThemeData = StreamGalleryHeaderThemeData; - -/// {@template gallery_header_theme_data} +/// {@template galleryHeaderThemeData} /// A style that overrides the default appearance of [GalleryHeader]s when used /// with [StreamGalleryHeaderTheme] or with the overall [StreamChatTheme]'s /// [StreamChatThemeData.galleryHeaderTheme]. @@ -112,16 +104,17 @@ class StreamGalleryHeaderThemeData with Diagnosticable { TextStyle? titleTextStyle, TextStyle? subtitleTextStyle, Color? bottomSheetBarrierColor, - }) => - StreamGalleryHeaderThemeData( - closeButtonColor: closeButtonColor ?? this.closeButtonColor, - backgroundColor: backgroundColor ?? this.backgroundColor, - iconMenuPointColor: iconMenuPointColor ?? this.iconMenuPointColor, - titleTextStyle: titleTextStyle ?? this.titleTextStyle, - subtitleTextStyle: subtitleTextStyle ?? this.subtitleTextStyle, - bottomSheetBarrierColor: - bottomSheetBarrierColor ?? this.bottomSheetBarrierColor, - ); + }) { + return StreamGalleryHeaderThemeData( + closeButtonColor: closeButtonColor ?? this.closeButtonColor, + backgroundColor: backgroundColor ?? this.backgroundColor, + iconMenuPointColor: iconMenuPointColor ?? this.iconMenuPointColor, + titleTextStyle: titleTextStyle ?? this.titleTextStyle, + subtitleTextStyle: subtitleTextStyle ?? this.subtitleTextStyle, + bottomSheetBarrierColor: + bottomSheetBarrierColor ?? this.bottomSheetBarrierColor, + ); + } /// Linearly interpolate between two [GalleryHeader] themes. /// @@ -130,18 +123,19 @@ class StreamGalleryHeaderThemeData with Diagnosticable { StreamGalleryHeaderThemeData a, StreamGalleryHeaderThemeData b, double t, - ) => - StreamGalleryHeaderThemeData( - closeButtonColor: Color.lerp(a.closeButtonColor, b.closeButtonColor, t), - backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), - iconMenuPointColor: - Color.lerp(a.iconMenuPointColor, b.iconMenuPointColor, t), - titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t), - subtitleTextStyle: - TextStyle.lerp(a.subtitleTextStyle, b.subtitleTextStyle, t), - bottomSheetBarrierColor: - Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t), - ); + ) { + return StreamGalleryHeaderThemeData( + closeButtonColor: Color.lerp(a.closeButtonColor, b.closeButtonColor, t), + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + iconMenuPointColor: + Color.lerp(a.iconMenuPointColor, b.iconMenuPointColor, t), + titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t), + subtitleTextStyle: + TextStyle.lerp(a.subtitleTextStyle, b.subtitleTextStyle, t), + bottomSheetBarrierColor: + Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t), + ); + } /// Merges one [StreamGalleryHeaderThemeData] with the another StreamGalleryHeaderThemeData merge(StreamGalleryHeaderThemeData? other) { diff --git a/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart index f99831a0..fe77d9c2 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart @@ -2,14 +2,10 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; -/// {@macro message_input_theme} -@Deprecated("Use 'StreamMessageInputTheme' instead") -typedef MessageInputTheme = StreamMessageInputTheme; - -/// {@template message_input_theme} +/// {@template messageInputTheme} /// Overrides the default style of [MessageInput] descendants. /// /// See also: @@ -55,11 +51,7 @@ class StreamMessageInputTheme extends InheritedTheme { data != oldWidget.data; } -/// {@macro message_input_theme_data} -@Deprecated("Use 'StreamMessageInputThemeData' instead") -typedef MessageInputThemeData = StreamMessageInputThemeData; - -/// {@template message_input_theme_data} +/// {@template messageInputThemeData} /// A style that overrides the default appearance of [MessageInput] widgets /// when used with [StreamMessageInputTheme] /// or with the overall [StreamChatTheme]'s @@ -153,60 +145,59 @@ class StreamMessageInputThemeData with Diagnosticable { bool? enableSafeArea, double? elevation, BoxShadow? shadow, - }) => - StreamMessageInputThemeData( - sendAnimationDuration: - sendAnimationDuration ?? this.sendAnimationDuration, - inputBackgroundColor: inputBackgroundColor ?? this.inputBackgroundColor, - actionButtonColor: actionButtonColor ?? this.actionButtonColor, - sendButtonColor: sendButtonColor ?? this.sendButtonColor, - actionButtonIdleColor: - actionButtonIdleColor ?? this.actionButtonIdleColor, - expandButtonColor: expandButtonColor ?? this.expandButtonColor, - inputTextStyle: inputTextStyle ?? this.inputTextStyle, - sendButtonIdleColor: sendButtonIdleColor ?? this.sendButtonIdleColor, - inputDecoration: inputDecoration ?? this.inputDecoration, - activeBorderGradient: activeBorderGradient ?? this.activeBorderGradient, - idleBorderGradient: idleBorderGradient ?? this.idleBorderGradient, - borderRadius: borderRadius ?? this.borderRadius, - linkHighlightColor: linkHighlightColor ?? this.linkHighlightColor, - enableSafeArea: enableSafeArea ?? this.enableSafeArea, - elevation: elevation ?? this.elevation, - shadow: shadow ?? this.shadow, - ); + }) { + return StreamMessageInputThemeData( + sendAnimationDuration: + sendAnimationDuration ?? this.sendAnimationDuration, + inputBackgroundColor: inputBackgroundColor ?? this.inputBackgroundColor, + actionButtonColor: actionButtonColor ?? this.actionButtonColor, + sendButtonColor: sendButtonColor ?? this.sendButtonColor, + actionButtonIdleColor: + actionButtonIdleColor ?? this.actionButtonIdleColor, + expandButtonColor: expandButtonColor ?? this.expandButtonColor, + inputTextStyle: inputTextStyle ?? this.inputTextStyle, + sendButtonIdleColor: sendButtonIdleColor ?? this.sendButtonIdleColor, + inputDecoration: inputDecoration ?? this.inputDecoration, + activeBorderGradient: activeBorderGradient ?? this.activeBorderGradient, + idleBorderGradient: idleBorderGradient ?? this.idleBorderGradient, + borderRadius: borderRadius ?? this.borderRadius, + enableSafeArea: enableSafeArea ?? this.enableSafeArea, + elevation: elevation ?? this.elevation, + shadow: shadow ?? this.shadow, + ); + } /// Linearly interpolate from one [StreamMessageInputThemeData] to another. StreamMessageInputThemeData lerp( StreamMessageInputThemeData a, StreamMessageInputThemeData b, double t, - ) => - StreamMessageInputThemeData( - actionButtonColor: - Color.lerp(a.actionButtonColor, b.actionButtonColor, t), - actionButtonIdleColor: - Color.lerp(a.actionButtonIdleColor, b.actionButtonIdleColor, t), - activeBorderGradient: - Gradient.lerp(a.activeBorderGradient, b.activeBorderGradient, t), - borderRadius: BorderRadius.lerp(a.borderRadius, b.borderRadius, t), - expandButtonColor: - Color.lerp(a.expandButtonColor, b.expandButtonColor, t), - idleBorderGradient: - Gradient.lerp(a.idleBorderGradient, b.idleBorderGradient, t), - inputBackgroundColor: - Color.lerp(a.inputBackgroundColor, b.inputBackgroundColor, t), - inputTextStyle: TextStyle.lerp(a.inputTextStyle, b.inputTextStyle, t), - sendButtonColor: Color.lerp(a.sendButtonColor, b.sendButtonColor, t), - sendButtonIdleColor: - Color.lerp(a.sendButtonIdleColor, b.sendButtonIdleColor, t), - sendAnimationDuration: a.sendAnimationDuration, - inputDecoration: a.inputDecoration, - linkHighlightColor: - Color.lerp(a.linkHighlightColor, b.linkHighlightColor, t), - enableSafeArea: a.enableSafeArea, - elevation: lerpDouble(a.elevation, b.elevation, t), - shadow: BoxShadow.lerp(a.shadow, b.shadow, t), - ); + ) { + return StreamMessageInputThemeData( + actionButtonColor: + Color.lerp(a.actionButtonColor, b.actionButtonColor, t), + actionButtonIdleColor: + Color.lerp(a.actionButtonIdleColor, b.actionButtonIdleColor, t), + activeBorderGradient: + Gradient.lerp(a.activeBorderGradient, b.activeBorderGradient, t), + borderRadius: BorderRadius.lerp(a.borderRadius, b.borderRadius, t), + expandButtonColor: + Color.lerp(a.expandButtonColor, b.expandButtonColor, t), + idleBorderGradient: + Gradient.lerp(a.idleBorderGradient, b.idleBorderGradient, t), + inputBackgroundColor: + Color.lerp(a.inputBackgroundColor, b.inputBackgroundColor, t), + inputTextStyle: TextStyle.lerp(a.inputTextStyle, b.inputTextStyle, t), + sendButtonColor: Color.lerp(a.sendButtonColor, b.sendButtonColor, t), + sendButtonIdleColor: + Color.lerp(a.sendButtonIdleColor, b.sendButtonIdleColor, t), + sendAnimationDuration: a.sendAnimationDuration, + inputDecoration: a.inputDecoration, + enableSafeArea: a.enableSafeArea, + elevation: lerpDouble(a.elevation, b.elevation, t), + shadow: BoxShadow.lerp(a.shadow, b.shadow, t), + ); + } /// Merges [this] [StreamMessageInputThemeData] with the [other] StreamMessageInputThemeData merge(StreamMessageInputThemeData? other) { diff --git a/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart index a8cda568..d21b3725 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart @@ -1,12 +1,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; -/// {@macro message_list_view_theme} -@Deprecated("Use 'StreamMessageListViewTheme' instead") -typedef MessageListViewTheme = StreamMessageListViewTheme; - -/// {@template message_list_view_theme} +/// {@template messageListViewTheme} /// Overrides the default style of [MessageListView] descendants. /// /// See also: @@ -52,11 +48,7 @@ class StreamMessageListViewTheme extends InheritedTheme { data != oldWidget.data; } -/// {@macro message_list_view_theme_data} -@Deprecated("Use 'StreamMessageListViewThemeData' instead") -typedef MessageListViewThemeData = StreamMessageListViewThemeData; - -/// {@template message_list_view_theme_data} +/// {@template messageListViewThemeData} /// A style that overrides the default appearance of [MessageListView]s when /// used with [StreamMessageListViewTheme] or with /// the overall [StreamChatTheme]'s @@ -87,11 +79,12 @@ class StreamMessageListViewThemeData with Diagnosticable { StreamMessageListViewThemeData copyWith({ Color? backgroundColor, DecorationImage? backgroundImage, - }) => - StreamMessageListViewThemeData( - backgroundColor: backgroundColor ?? this.backgroundColor, - backgroundImage: backgroundImage ?? this.backgroundImage, - ); + }) { + return StreamMessageListViewThemeData( + backgroundColor: backgroundColor ?? this.backgroundColor, + backgroundImage: backgroundImage ?? this.backgroundImage, + ); + } /// Linearly interpolate between two [MessageListView] themes. /// @@ -100,11 +93,12 @@ class StreamMessageListViewThemeData with Diagnosticable { StreamMessageListViewThemeData a, StreamMessageListViewThemeData b, double t, - ) => - StreamMessageListViewThemeData( - backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), - backgroundImage: t < 0.5 ? a.backgroundImage : b.backgroundImage, - ); + ) { + return StreamMessageListViewThemeData( + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + backgroundImage: t < 0.5 ? a.backgroundImage : b.backgroundImage, + ); + } /// Merges one [StreamMessageListViewThemeData] with another. StreamMessageListViewThemeData merge(StreamMessageListViewThemeData? other) { diff --git a/packages/stream_chat_flutter/lib/src/theme/message_search_list_view_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_search_list_view_theme.dart deleted file mode 100644 index e88cb96c..00000000 --- a/packages/stream_chat_flutter/lib/src/theme/message_search_list_view_theme.dart +++ /dev/null @@ -1,126 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; - -/// {@macro message_search_list_view_theme} -@Deprecated("Use 'StreamMessageSearchListViewTheme' instead") -typedef MessageSearchListViewTheme = StreamMessageSearchListViewTheme; - -/// {@template message_search_list_view_theme} -/// Overrides the default style of [MessageSearchListView] descendants. -/// -/// See also: -/// -/// * [UserListViewThemeData], which is used to configure this theme. -/// {@endtemplate} -class StreamMessageSearchListViewTheme extends InheritedTheme { - /// Creates a [UserListViewTheme]. - /// - /// The [data] parameter must not be null. - const StreamMessageSearchListViewTheme({ - super.key, - required this.data, - required super.child, - }); - - /// The configuration of this theme. - final StreamMessageSearchListViewThemeData data; - - /// The closest instance of this class that encloses the given context. - /// - /// If there is no enclosing [MessageSearchListView] widget, then - /// [StreamChatThemeData.messageSearchListViewTheme] is used. - /// - /// Typical usage is as follows: - /// - /// ```dart - /// MessageSearchListViewTheme theme = MessageSearchListViewTheme.of(context); - /// ``` - static StreamMessageSearchListViewThemeData of(BuildContext context) { - final messageSearchListViewTheme = context - .dependOnInheritedWidgetOfExactType(); - return messageSearchListViewTheme?.data ?? - StreamChatTheme.of(context).messageSearchListViewTheme; - } - - @override - Widget wrap(BuildContext context, Widget child) => - StreamMessageSearchListViewTheme(data: data, child: child); - - @override - bool updateShouldNotify(StreamMessageSearchListViewTheme oldWidget) => - data != oldWidget.data; -} - -/// {@macro message_search_list_view_theme_data} -@Deprecated("Use 'StreamMessageSearchListViewThemeData' instead") -typedef MessageSearchListViewThemeData = StreamMessageSearchListViewThemeData; - -/// {@macro message_search_list_view_theme_data} -/// A style that overrides the default appearance of [MessageSearchListView]s -/// when used with [MessageSearchListView] or with the overall -/// [StreamChatTheme]'s [StreamChatThemeData.messageSearchListViewTheme]. -/// -/// See also: -/// -/// * [StreamMessageSearchListViewTheme], the theme -/// which is configured with this class. -/// * [StreamChatThemeData.messageSearchListViewTheme], which can be used to -/// override the default style for [UserListView]s below the overall -/// [StreamChatTheme]. -/// {@endtemplate} -class StreamMessageSearchListViewThemeData with Diagnosticable { - /// Creates a [StreamMessageSearchListViewThemeData]. - const StreamMessageSearchListViewThemeData({ - this.backgroundColor, - }); - - /// The color of the [MessageSearchListView] background. - final Color? backgroundColor; - - /// Copies this [StreamMessageSearchListViewThemeData] to another. - StreamMessageSearchListViewThemeData copyWith({ - Color? backgroundColor, - }) => - StreamMessageSearchListViewThemeData( - backgroundColor: backgroundColor ?? this.backgroundColor, - ); - - /// Linearly interpolate between two [UserListViewThemeData] themes. - /// - /// All the properties must be non-null. - StreamMessageSearchListViewThemeData lerp( - StreamMessageSearchListViewThemeData a, - StreamMessageSearchListViewThemeData b, - double t, - ) => - StreamMessageSearchListViewThemeData( - backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), - ); - - /// Merges one [StreamMessageSearchListViewThemeData] with another. - StreamMessageSearchListViewThemeData merge( - StreamMessageSearchListViewThemeData? other, - ) { - if (other == null) return this; - return copyWith( - backgroundColor: other.backgroundColor, - ); - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is StreamMessageSearchListViewThemeData && - runtimeType == other.runtimeType && - backgroundColor == other.backgroundColor; - - @override - int get hashCode => backgroundColor.hashCode; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(ColorProperty('backgroundColor', backgroundColor)); - } -} diff --git a/packages/stream_chat_flutter/lib/src/theme/message_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_theme.dart index c94c8159..f83971f2 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_theme.dart @@ -2,10 +2,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; -/// {@macro message_theme_data} -@Deprecated("Use 'StreamMessageThemeData' instead") -typedef MessageThemeData = StreamMessageThemeData; - /// {@template message_theme_data} /// Class for getting message theme /// {@endtemplate} @@ -77,57 +73,59 @@ class StreamMessageThemeData with Diagnosticable { Color? reactionsBorderColor, Color? reactionsMaskColor, Color? linkBackgroundColor, - }) => - StreamMessageThemeData( - messageTextStyle: messageTextStyle ?? this.messageTextStyle, - messageAuthorStyle: messageAuthorStyle ?? this.messageAuthorStyle, - messageLinksStyle: messageLinksStyle ?? this.messageLinksStyle, - createdAtStyle: createdAtStyle ?? this.createdAtStyle, - messageBackgroundColor: - messageBackgroundColor ?? this.messageBackgroundColor, - messageBorderColor: messageBorderColor ?? this.messageBorderColor, - avatarTheme: avatarTheme ?? this.avatarTheme, - repliesStyle: repliesStyle ?? this.repliesStyle, - reactionsBackgroundColor: - reactionsBackgroundColor ?? this.reactionsBackgroundColor, - reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor, - reactionsMaskColor: reactionsMaskColor ?? this.reactionsMaskColor, - linkBackgroundColor: linkBackgroundColor ?? this.linkBackgroundColor, - ); + }) { + return StreamMessageThemeData( + messageTextStyle: messageTextStyle ?? this.messageTextStyle, + messageAuthorStyle: messageAuthorStyle ?? this.messageAuthorStyle, + messageLinksStyle: messageLinksStyle ?? this.messageLinksStyle, + createdAtStyle: createdAtStyle ?? this.createdAtStyle, + messageBackgroundColor: + messageBackgroundColor ?? this.messageBackgroundColor, + messageBorderColor: messageBorderColor ?? this.messageBorderColor, + avatarTheme: avatarTheme ?? this.avatarTheme, + repliesStyle: repliesStyle ?? this.repliesStyle, + reactionsBackgroundColor: + reactionsBackgroundColor ?? this.reactionsBackgroundColor, + reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor, + reactionsMaskColor: reactionsMaskColor ?? this.reactionsMaskColor, + linkBackgroundColor: linkBackgroundColor ?? this.linkBackgroundColor, + ); + } /// Linearly interpolate from one [StreamMessageThemeData] to another. StreamMessageThemeData lerp( StreamMessageThemeData a, StreamMessageThemeData b, double t, - ) => - StreamMessageThemeData( - avatarTheme: const StreamAvatarThemeData() - .lerp(a.avatarTheme!, b.avatarTheme!, t), - createdAtStyle: TextStyle.lerp(a.createdAtStyle, b.createdAtStyle, t), - messageAuthorStyle: - TextStyle.lerp(a.messageAuthorStyle, b.messageAuthorStyle, t), - messageBackgroundColor: - Color.lerp(a.messageBackgroundColor, b.messageBackgroundColor, t), - messageBorderColor: - Color.lerp(a.messageBorderColor, b.messageBorderColor, t), - messageLinksStyle: - TextStyle.lerp(a.messageLinksStyle, b.messageLinksStyle, t), - messageTextStyle: - TextStyle.lerp(a.messageTextStyle, b.messageTextStyle, t), - reactionsBackgroundColor: Color.lerp( - a.reactionsBackgroundColor, - b.reactionsBackgroundColor, - t, - ), - reactionsBorderColor: - Color.lerp(a.messageBorderColor, b.reactionsBorderColor, t), - reactionsMaskColor: - Color.lerp(a.reactionsMaskColor, b.reactionsMaskColor, t), - repliesStyle: TextStyle.lerp(a.repliesStyle, b.repliesStyle, t), - linkBackgroundColor: - Color.lerp(a.linkBackgroundColor, b.linkBackgroundColor, t), - ); + ) { + return StreamMessageThemeData( + avatarTheme: + const StreamAvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), + createdAtStyle: TextStyle.lerp(a.createdAtStyle, b.createdAtStyle, t), + messageAuthorStyle: + TextStyle.lerp(a.messageAuthorStyle, b.messageAuthorStyle, t), + messageBackgroundColor: + Color.lerp(a.messageBackgroundColor, b.messageBackgroundColor, t), + messageBorderColor: + Color.lerp(a.messageBorderColor, b.messageBorderColor, t), + messageLinksStyle: + TextStyle.lerp(a.messageLinksStyle, b.messageLinksStyle, t), + messageTextStyle: + TextStyle.lerp(a.messageTextStyle, b.messageTextStyle, t), + reactionsBackgroundColor: Color.lerp( + a.reactionsBackgroundColor, + b.reactionsBackgroundColor, + t, + ), + reactionsBorderColor: + Color.lerp(a.messageBorderColor, b.reactionsBorderColor, t), + reactionsMaskColor: + Color.lerp(a.reactionsMaskColor, b.reactionsMaskColor, t), + repliesStyle: TextStyle.lerp(a.repliesStyle, b.repliesStyle, t), + linkBackgroundColor: + Color.lerp(a.linkBackgroundColor, b.linkBackgroundColor, t), + ); + } /// Merge with a theme StreamMessageThemeData merge(StreamMessageThemeData? other) { diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart similarity index 71% rename from packages/stream_chat_flutter/lib/src/stream_chat_theme.dart rename to packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart index e3f44d22..8021406f 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart @@ -1,16 +1,18 @@ import 'package:flutter/material.dart' hide TextTheme; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@template streamChatTheme} /// Inherited widget providing the [StreamChatThemeData] to the widget tree +/// {@endtemplate} class StreamChatTheme extends InheritedWidget { - /// Constructor for creating a [StreamChatTheme] + /// {@macro streamChatTheme} const StreamChatTheme({ super.key, required this.data, required super.child, }); - /// Theme data + /// {@macro streamChatThemeData} final StreamChatThemeData data; @override @@ -30,9 +32,11 @@ class StreamChatTheme extends InheritedWidget { } } -/// Theme data +/// {@template streamChatThemeData} +/// Theme data for Stream Chat +/// {@endtemplate} class StreamChatThemeData { - /// Create a theme from scratch + /// Creates a theme from scratch factory StreamChatThemeData({ Brightness? brightness, StreamTextTheme? textTheme, @@ -44,15 +48,12 @@ class StreamChatThemeData { StreamMessageThemeData? ownMessageTheme, StreamMessageInputThemeData? messageInputTheme, Widget Function(BuildContext, User)? defaultUserImage, - Widget Function(BuildContext, User)? placeholderUserImage, + PlaceholderUserImage? placeholderUserImage, IconThemeData? primaryIconTheme, List? reactionIcons, StreamGalleryHeaderThemeData? imageHeaderTheme, StreamGalleryFooterThemeData? imageFooterTheme, StreamMessageListViewThemeData? messageListViewTheme, - StreamChannelListViewThemeData? channelListViewTheme, - StreamUserListViewThemeData? userListViewTheme, - StreamMessageSearchListViewThemeData? messageSearchListViewTheme, }) { brightness ??= colorTheme?.brightness ?? Brightness.light; final isDark = brightness == Brightness.dark; @@ -78,23 +79,20 @@ class StreamChatThemeData { galleryHeaderTheme: imageHeaderTheme, galleryFooterTheme: imageFooterTheme, messageListViewTheme: messageListViewTheme, - channelListViewTheme: channelListViewTheme, - userListViewTheme: userListViewTheme, - messageSearchListViewTheme: messageSearchListViewTheme, ); return defaultData.merge(customizedData); } - /// Theme initialised with light + /// Theme initialized with light factory StreamChatThemeData.light() => StreamChatThemeData(brightness: Brightness.light); - /// Theme initialised with dark + /// Theme initialized with dark factory StreamChatThemeData.dark() => StreamChatThemeData(brightness: Brightness.dark); - /// Raw theme init + /// Raw theme initialization const StreamChatThemeData.raw({ required this.textTheme, required this.colorTheme, @@ -104,19 +102,13 @@ class StreamChatThemeData { required this.otherMessageTheme, required this.ownMessageTheme, required this.messageInputTheme, - required this.defaultUserImage, - this.placeholderUserImage, required this.primaryIconTheme, - required this.reactionIcons, required this.galleryHeaderTheme, required this.galleryFooterTheme, required this.messageListViewTheme, - required this.channelListViewTheme, - required this.userListViewTheme, - required this.messageSearchListViewTheme, }); - /// Create a theme from a Material [Theme] + /// Creates a theme from a Material [Theme] factory StreamChatThemeData.fromTheme(ThemeData theme) { final defaultTheme = StreamChatThemeData(brightness: theme.brightness); final customizedTheme = StreamChatThemeData.fromColorAndTextTheme( @@ -128,7 +120,7 @@ class StreamChatThemeData { return defaultTheme.merge(customizedTheme); } - /// Create theme from color and text theme + /// Creates a theme from a [StreamColorTheme] and a [StreamTextTheme] factory StreamChatThemeData.fromColorAndTextTheme( StreamColorTheme colorTheme, StreamTextTheme textTheme, @@ -172,12 +164,6 @@ class StreamChatThemeData { textTheme: textTheme, colorTheme: colorTheme, primaryIconTheme: iconTheme, - defaultUserImage: (context, user) => Center( - child: StreamGradientAvatar( - name: user.name, - userId: user.id, - ), - ), channelPreviewTheme: channelPreviewTheme, channelListHeaderTheme: StreamChannelListHeaderThemeData( avatarTheme: StreamAvatarThemeData( @@ -263,68 +249,6 @@ class StreamChatThemeData { ], ), ), - reactionIcons: [ - StreamReactionIcon( - type: 'love', - builder: (context, highlighted, size) { - final theme = StreamChatTheme.of(context); - return StreamSvgIcon.loveReaction( - color: highlighted - ? theme.colorTheme.accentPrimary - : theme.primaryIconTheme.color!.withOpacity(0.5), - size: size, - ); - }, - ), - StreamReactionIcon( - type: 'like', - builder: (context, highlighted, size) { - final theme = StreamChatTheme.of(context); - return StreamSvgIcon.thumbsUpReaction( - color: highlighted - ? theme.colorTheme.accentPrimary - : theme.primaryIconTheme.color!.withOpacity(0.5), - size: size, - ); - }, - ), - StreamReactionIcon( - type: 'sad', - builder: (context, highlighted, size) { - final theme = StreamChatTheme.of(context); - return StreamSvgIcon.thumbsDownReaction( - color: highlighted - ? theme.colorTheme.accentPrimary - : theme.primaryIconTheme.color!.withOpacity(0.5), - size: size, - ); - }, - ), - StreamReactionIcon( - type: 'haha', - builder: (context, highlighted, size) { - final theme = StreamChatTheme.of(context); - return StreamSvgIcon.lolReaction( - color: highlighted - ? theme.colorTheme.accentPrimary - : theme.primaryIconTheme.color!.withOpacity(0.5), - size: size, - ); - }, - ), - StreamReactionIcon( - type: 'wow', - builder: (context, highlighted, size) { - final theme = StreamChatTheme.of(context); - return StreamSvgIcon.wutReaction( - color: highlighted - ? theme.colorTheme.accentPrimary - : theme.primaryIconTheme.color!.withOpacity(0.5), - size: size, - ); - }, - ), - ], galleryHeaderTheme: StreamGalleryHeaderThemeData( closeButtonColor: colorTheme.textHighEmphasis, backgroundColor: channelHeaderTheme.color, @@ -346,15 +270,6 @@ class StreamChatThemeData { messageListViewTheme: StreamMessageListViewThemeData( backgroundColor: colorTheme.barsBg, ), - channelListViewTheme: StreamChannelListViewThemeData( - backgroundColor: colorTheme.appBg, - ), - userListViewTheme: StreamUserListViewThemeData( - backgroundColor: colorTheme.appBg, - ), - messageSearchListViewTheme: StreamMessageSearchListViewThemeData( - backgroundColor: colorTheme.appBg, - ), ); } @@ -390,30 +305,12 @@ class StreamChatThemeData { /// Theme dedicated to the [StreamMessageInput] widget final StreamMessageInputThemeData messageInputTheme; - /// The widget that will be built when the user image is unavailable - final Widget Function(BuildContext, User) defaultUserImage; - - /// The widget that will be built when the user image is loading - final Widget Function(BuildContext, User)? placeholderUserImage; - /// Primary icon theme final IconThemeData primaryIconTheme; - /// Assets used for rendering reactions - final List reactionIcons; - /// Theme configuration for the [StreamMessageListView] widget. final StreamMessageListViewThemeData messageListViewTheme; - /// Theme configuration for the [StreamChannelListView] widget. - final StreamChannelListViewThemeData channelListViewTheme; - - /// Theme configuration for the [StreamUserListView] widget. - final StreamUserListViewThemeData userListViewTheme; - - /// Theme configuration for the [StreamMessageSearchListView] widget. - final StreamMessageSearchListViewThemeData messageSearchListViewTheme; - /// Creates a copy of [StreamChatThemeData] with specified attributes /// overridden. StreamChatThemeData copyWith({ @@ -425,16 +322,13 @@ class StreamChatThemeData { StreamMessageThemeData? otherMessageTheme, StreamMessageInputThemeData? messageInputTheme, Widget Function(BuildContext, User)? defaultUserImage, - Widget Function(BuildContext, User)? placeholderUserImage, + PlaceholderUserImage? placeholderUserImage, IconThemeData? primaryIconTheme, StreamChannelListHeaderThemeData? channelListHeaderTheme, List? reactionIcons, StreamGalleryHeaderThemeData? galleryHeaderTheme, StreamGalleryFooterThemeData? galleryFooterTheme, StreamMessageListViewThemeData? messageListViewTheme, - StreamChannelListViewThemeData? channelListViewTheme, - StreamUserListViewThemeData? userListViewTheme, - StreamMessageSearchListViewThemeData? messageSearchListViewTheme, }) => StreamChatThemeData.raw( channelListHeaderTheme: @@ -442,22 +336,15 @@ class StreamChatThemeData { textTheme: this.textTheme.merge(textTheme), colorTheme: this.colorTheme.merge(colorTheme), primaryIconTheme: this.primaryIconTheme.merge(primaryIconTheme), - defaultUserImage: defaultUserImage ?? this.defaultUserImage, - placeholderUserImage: placeholderUserImage ?? this.placeholderUserImage, channelPreviewTheme: this.channelPreviewTheme.merge(channelPreviewTheme), channelHeaderTheme: this.channelHeaderTheme.merge(channelHeaderTheme), ownMessageTheme: this.ownMessageTheme.merge(ownMessageTheme), otherMessageTheme: this.otherMessageTheme.merge(otherMessageTheme), messageInputTheme: this.messageInputTheme.merge(messageInputTheme), - reactionIcons: reactionIcons ?? this.reactionIcons, galleryHeaderTheme: galleryHeaderTheme ?? this.galleryHeaderTheme, galleryFooterTheme: galleryFooterTheme ?? this.galleryFooterTheme, messageListViewTheme: messageListViewTheme ?? this.messageListViewTheme, - channelListViewTheme: channelListViewTheme ?? this.channelListViewTheme, - userListViewTheme: userListViewTheme ?? this.userListViewTheme, - messageSearchListViewTheme: - messageSearchListViewTheme ?? this.messageSearchListViewTheme, ); /// Merge themes @@ -469,23 +356,15 @@ class StreamChatThemeData { textTheme: textTheme.merge(other.textTheme), colorTheme: colorTheme.merge(other.colorTheme), primaryIconTheme: other.primaryIconTheme, - defaultUserImage: other.defaultUserImage, - placeholderUserImage: other.placeholderUserImage, channelPreviewTheme: channelPreviewTheme.merge(other.channelPreviewTheme), channelHeaderTheme: channelHeaderTheme.merge(other.channelHeaderTheme), ownMessageTheme: ownMessageTheme.merge(other.ownMessageTheme), otherMessageTheme: otherMessageTheme.merge(other.otherMessageTheme), messageInputTheme: messageInputTheme.merge(other.messageInputTheme), - reactionIcons: other.reactionIcons, galleryHeaderTheme: galleryHeaderTheme.merge(other.galleryHeaderTheme), galleryFooterTheme: galleryFooterTheme.merge(other.galleryFooterTheme), messageListViewTheme: messageListViewTheme.merge(other.messageListViewTheme), - channelListViewTheme: - channelListViewTheme.merge(other.channelListViewTheme), - userListViewTheme: userListViewTheme.merge(other.userListViewTheme), - messageSearchListViewTheme: - messageSearchListViewTheme.merge(other.messageSearchListViewTheme), ); } } diff --git a/packages/stream_chat_flutter/lib/src/theme/text_theme.dart b/packages/stream_chat_flutter/lib/src/theme/text_theme.dart index 06ddf2d9..e44dcc20 100644 --- a/packages/stream_chat_flutter/lib/src/theme/text_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/text_theme.dart @@ -1,9 +1,5 @@ import 'package:flutter/material.dart'; -/// {@macro text_theme} -@Deprecated("Use 'StreamTextTheme' instead") -typedef TextTheme = StreamTextTheme; - /// {@template text_theme} /// Class for holding text theme /// {@endtemplate} diff --git a/packages/stream_chat_flutter/lib/src/theme/themes.dart b/packages/stream_chat_flutter/lib/src/theme/themes.dart index 7e2fb0aa..9e3f2dd9 100644 --- a/packages/stream_chat_flutter/lib/src/theme/themes.dart +++ b/packages/stream_chat_flutter/lib/src/theme/themes.dart @@ -1,14 +1,11 @@ export 'avatar_theme.dart'; export 'channel_header_theme.dart'; export 'channel_list_header_theme.dart'; -export 'channel_list_view_theme.dart'; export 'channel_preview_theme.dart'; export 'color_theme.dart'; export 'gallery_footer_theme.dart'; export 'gallery_header_theme.dart'; export 'message_input_theme.dart'; export 'message_list_view_theme.dart'; -export 'message_search_list_view_theme.dart'; export 'message_theme.dart'; export 'text_theme.dart'; -export 'user_list_view_theme.dart'; diff --git a/packages/stream_chat_flutter/lib/src/theme/user_list_view_theme.dart b/packages/stream_chat_flutter/lib/src/theme/user_list_view_theme.dart deleted file mode 100644 index 325f9d88..00000000 --- a/packages/stream_chat_flutter/lib/src/theme/user_list_view_theme.dart +++ /dev/null @@ -1,123 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; - -/// {@macro user_list_view_theme} -@Deprecated("Use 'StreamUserListViewTheme' instead") -typedef UserListViewTheme = StreamUserListViewTheme; - -/// {@template user_list_view_theme} -/// Overrides the default style of [UserListView] descendants. -/// -/// See also: -/// -/// * [StreamUserListViewThemeData], which is used to configure this theme. -/// {@endtemplate} -class StreamUserListViewTheme extends InheritedTheme { - /// Creates a [StreamUserListViewTheme]. - /// - /// The [data] parameter must not be null. - const StreamUserListViewTheme({ - super.key, - required this.data, - required super.child, - }); - - /// The configuration of this theme. - final StreamUserListViewThemeData data; - - /// The closest instance of this class that encloses the given context. - /// - /// If there is no enclosing [StreamUserListViewTheme] widget, then - /// [StreamChatThemeData.userListViewTheme] is used. - /// - /// Typical usage is as follows: - /// - /// ```dart - /// UserListViewTheme theme = UserListViewTheme.of(context); - /// ``` - static StreamUserListViewThemeData of(BuildContext context) { - final userListViewTheme = - context.dependOnInheritedWidgetOfExactType(); - return userListViewTheme?.data ?? - StreamChatTheme.of(context).userListViewTheme; - } - - @override - Widget wrap(BuildContext context, Widget child) => - StreamUserListViewTheme(data: data, child: child); - - @override - bool updateShouldNotify(StreamUserListViewTheme oldWidget) => - data != oldWidget.data; -} - -/// {@macro user_list_view_theme_data} -@Deprecated("Use 'StreamUserListViewThemeData' instead") -typedef UserListViewThemeData = StreamUserListViewThemeData; - -/// {@template user_list_view_theme_data} -/// A style that overrides the default appearance of [UserListView]s when -/// used with [StreamUserListViewTheme] or with the overall [StreamChatTheme]'s -/// [StreamChatThemeData.userListViewTheme]. -/// -/// See also: -/// -/// * [StreamUserListViewTheme], the theme which is configured with this class. -/// * [StreamChatThemeData.userListViewTheme], which can be used to override -/// the default style for [UserListView]s below the overall -/// [StreamChatTheme]. -/// {@endtemplate} -class StreamUserListViewThemeData with Diagnosticable { - /// Creates a [StreamUserListViewThemeData]. - const StreamUserListViewThemeData({ - this.backgroundColor, - }); - - /// The color of the [ChannelListView] background. - final Color? backgroundColor; - - /// Copies this [ChannelListViewThemeData] to another. - StreamUserListViewThemeData copyWith({ - Color? backgroundColor, - }) => - StreamUserListViewThemeData( - backgroundColor: backgroundColor ?? this.backgroundColor, - ); - - /// Linearly interpolate between two [StreamUserListViewThemeData] themes. - /// - /// All the properties must be non-null. - StreamUserListViewThemeData lerp( - StreamUserListViewThemeData a, - StreamUserListViewThemeData b, - double t, - ) => - StreamUserListViewThemeData( - backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), - ); - - /// Merges one [StreamUserListViewThemeData] with another. - StreamUserListViewThemeData merge(StreamUserListViewThemeData? other) { - if (other == null) return this; - return copyWith( - backgroundColor: other.backgroundColor, - ); - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is StreamUserListViewThemeData && - runtimeType == other.runtimeType && - backgroundColor == other.backgroundColor; - - @override - int get hashCode => backgroundColor.hashCode; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(ColorProperty('backgroundColor', backgroundColor)); - } -} diff --git a/packages/stream_chat_flutter/lib/src/user_item.dart b/packages/stream_chat_flutter/lib/src/user/user_item.dart similarity index 77% rename from packages/stream_chat_flutter/lib/src/user_item.dart rename to packages/stream_chat_flutter/lib/src/user/user_item.dart index e537f5d4..348440a7 100644 --- a/packages/stream_chat_flutter/lib/src/user_item.dart +++ b/packages/stream_chat_flutter/lib/src/user/user_item.dart @@ -1,26 +1,22 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@macro user_item} -@Deprecated("Use 'StreamUserItem' instead") -typedef UserItem = StreamUserItem; - -/// {@template user_item} -/// It shows the current [User] preview. +/// {@template streamUserItem} +/// Shows a preview of the current [User]. /// -/// The widget uses a [StreamBuilder] to render the user information +/// This widget uses a [StreamBuilder] to render the user information /// image as soon as it updates. /// -/// Usually you don't use this widget as it's the default user preview used -/// by [StreamUserListView]. +/// It is not recommended to use this widget as it's the default user preview +/// used by [StreamUserListView]. /// /// The widget renders the ui based on the first ancestor of type /// [StreamChatTheme]. -/// Modify it to change the widget appearance. +/// Modify it to change the widget's appearance. /// {@endtemplate} class StreamUserItem extends StatelessWidget { - /// Instantiate a new UserItem + /// {@macro streamUserItem} const StreamUserItem({ super.key, required this.user, @@ -31,16 +27,16 @@ class StreamUserItem extends StatelessWidget { this.showLastOnline = true, }); - /// Function called when tapping this widget + /// Function called when tapping or clicking on this widget final void Function(User)? onTap; /// Function called when long pressing this widget final void Function(User)? onLongPress; - /// User displayed + /// The user to display final User user; - /// The function called when the image is tapped + /// The function called when the image is tapped or clicked final void Function(User)? onImageTap; /// If true the [StreamUserItem] will show a trailing checkmark diff --git a/packages/stream_chat_flutter/lib/src/user_mention_tile.dart b/packages/stream_chat_flutter/lib/src/user/user_mention_tile.dart similarity index 89% rename from packages/stream_chat_flutter/lib/src/user_mention_tile.dart rename to packages/stream_chat_flutter/lib/src/user/user_mention_tile.dart index fd62d013..8e9d948d 100644 --- a/packages/stream_chat_flutter/lib/src/user_mention_tile.dart +++ b/packages/stream_chat_flutter/lib/src/user/user_mention_tile.dart @@ -1,17 +1,14 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@macro user_mention_tile} -@Deprecated("Use 'StreamUserMentionTile' instead") -typedef UserMentionTile = StreamUserMentionTile; - -/// {@template user_mention_tile} -/// This widget is used for showing user tiles for mentions +/// {@template streamUserMentionTile} +/// Shows user tiles for mentions. +/// /// Use [title], [subtitle], [leading], [trailing] for /// substituting widgets in respective positions /// {@endtemplate} class StreamUserMentionTile extends StatelessWidget { - /// Constructor for creating a [StreamUserMentionTile] widget + /// {@macro streamUserMentionTile} const StreamUserMentionTile( this.user, { super.key, diff --git a/packages/stream_chat_flutter/lib/src/user_list_view.dart b/packages/stream_chat_flutter/lib/src/user_list_view.dart deleted file mode 100644 index 55a77322..00000000 --- a/packages/stream_chat_flutter/lib/src/user_list_view.dart +++ /dev/null @@ -1,446 +0,0 @@ -// ignore: lines_longer_than_80_chars -// ignore_for_file: deprecated_member_use_from_same_package, deprecated_member_use - -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// Callback called when tapping on a user -typedef UserTapCallback = void Function(User, Widget?); - -/// Builder used to create a custom [ListUserItem] from a [User] -typedef UserItemBuilder = Widget Function(BuildContext, User, bool); - -/// {@template user_list_view} -/// It shows the list of current users. -/// -/// ```dart -/// class UsersListPage extends StatelessWidget { -/// @override -/// Widget build(BuildContext context) { -/// return Scaffold( -/// body: UsersListView( -/// filter: { -/// 'members': { -/// '\$in': [StreamChat.of(context).user.id], -/// } -/// }, -/// sort: [SortOption('last_message_at')], -/// pagination: PaginationParams( -/// limit: 20, -/// ), -/// channelWidget: ChannelPage(), -/// ), -/// ); -/// } -/// } -/// ``` -/// -/// -/// Make sure to have a [UsersBloc] ancestor in order to provide the -/// information about the users. -/// The widget uses a [ListView.separated], [GridView.builder] to render the -/// list, grid of channels. -/// -/// The widget components render the ui based on the first ancestor of -/// type [StreamChatTheme]. -/// Modify it to change the widget appearance. -/// {@endtemplate} -@Deprecated("Use 'StreamUserListView' instead") -class UserListView extends StatefulWidget { - /// Instantiate a new UserListView - @Deprecated("Use 'StreamUserListView' instead") - UserListView({ - super.key, - this.filter = const Filter.empty(), - this.sort, - this.presence, - @Deprecated( - "'pagination' is deprecated and shouldn't be used. " - "This property is no longer used, Please use 'limit' instead", - ) - this.pagination, - int? limit, - this.onUserTap, - this.onUserLongPress, - this.userWidget, - this.userItemBuilder, - this.separatorBuilder, - this.onImageTap, - this.selectedUsers, - this.pullToRefresh = true, - this.groupAlphabetically = false, - this.crossAxisCount = 1, - this.errorBuilder, - this.emptyBuilder, - this.loadingBuilder, - this.listBuilder, - this.userListController, - }) : assert( - crossAxisCount == 1 || !groupAlphabetically, - 'Cannot group alphabetically when crossAxisCount > 1', - ), - limit = limit ?? pagination?.limit ?? 30; - - /// The query filters to use. - /// You can query on any of the custom fields you've defined on the [Channel]. - /// You can also filter other built-in channel fields. - final Filter filter; - - /// The sorting used for the channels matching the filters. - /// Sorting is based on field and direction, multiple sorting options can - /// be provided. - /// You can sort based on last_updated, last_message_at, updated_at, created_ - /// at or member_count. - /// Direction can be ascending or descending. - final List? sort; - - /// If true you’ll receive user presence updates via the websocket events - final bool? presence; - - /// Pagination parameters - /// limit: the number of users to return (max is 30) - /// offset: the offset (max is 1000) - /// message_limit: how many messages should be included to each channel - @Deprecated( - "'pagination' is deprecated and shouldn't be used. " - "This property is no longer used, Please use 'limit' instead", - ) - final PaginationParams? pagination; - - /// The amount of users requested per API call. - final int limit; - - /// Function called when tapping on a channel - /// By default it calls [Navigator.push] building a [MaterialPageRoute] - /// with the widget [userWidget] as child. - final UserTapCallback? onUserTap; - - /// Function called when long pressing on a channel - final Function(User)? onUserLongPress; - - /// Widget used when opening a channel - final Widget? userWidget; - - /// Builder used to create a custom user preview - final UserItemBuilder? userItemBuilder; - - /// Builder used to create a custom item separator - final Function(BuildContext, int)? separatorBuilder; - - /// The function called when the image is tapped - final Function(User)? onImageTap; - - /// Set it to false to disable the pull-to-refresh widget - final bool pullToRefresh; - - /// Sets a blue trailing checkMark in [ListUserItem] for all the - /// [selectedUsers] - final Set? selectedUsers; - - /// Set it to true to group users by their first character - /// - /// defaults to false - final bool groupAlphabetically; - - /// The number of children in the cross axis. - final int crossAxisCount; - - /// The builder that will be used in case of error - final ErrorBuilder? errorBuilder; - - /// The builder that will be used to build the list - final Widget Function(BuildContext context, List users)? - listBuilder; - - /// The builder that will be used for loading - final WidgetBuilder? loadingBuilder; - - /// The builder used when the channel list is empty. - final WidgetBuilder? emptyBuilder; - - /// A [UserListController] allows reloading and pagination. - /// Use [UserListController.loadData] and [UserListController.paginateData] - /// respectively for reloading and pagination. - final UserListController? userListController; - - @override - _UserListViewState createState() => _UserListViewState(); -} - -class _UserListViewState extends State - with WidgetsBindingObserver { - bool get _isListView => widget.crossAxisCount == 1; - - late final _defaultController = UserListController(); - - UserListController get _userListController => - widget.userListController ?? _defaultController; - - @override - Widget build(BuildContext context) { - final userListCore = UserListCore( - errorBuilder: widget.errorBuilder ?? - (BuildContext context, Object err) => _buildError(err), - emptyBuilder: widget.emptyBuilder ?? (context) => _buildEmpty(), - loadingBuilder: widget.loadingBuilder ?? - (context) => LayoutBuilder( - builder: (context, viewportConstraints) => - SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: const Center( - child: CircularProgressIndicator(), - ), - ), - ), - ), - listBuilder: - widget.listBuilder ?? (context, list) => _buildListView(list), - limit: widget.limit, - sort: widget.sort, - filter: widget.filter, - presence: widget.presence, - groupAlphabetically: widget.groupAlphabetically, - userListController: _userListController, - ); - - final backgroundColor = StreamUserListViewTheme.of(context).backgroundColor; - - Widget child; - - if (backgroundColor != null) { - child = ColoredBox( - color: backgroundColor, - child: userListCore, - ); - } else { - child = userListCore; - } - - if (!widget.pullToRefresh) { - return child; - } else { - return RefreshIndicator( - onRefresh: () => _userListController.loadData!(), - child: child, - ); - } - } - - bool get isListAlreadySorted => - widget.sort?.any((e) => e.field == 'name' && e.direction == 1) ?? false; - - Widget _buildError(Object error) => Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text.rich( - TextSpan( - children: [ - const WidgetSpan( - child: Padding( - padding: EdgeInsets.only( - right: 2, - ), - child: Icon(Icons.error_outline), - ), - ), - TextSpan(text: context.translations.loadingUsersError), - ], - ), - style: Theme.of(context).textTheme.headline6, - ), - TextButton( - onPressed: () => _userListController.loadData!(), - child: Text(context.translations.retryLabel), - ), - ], - ), - ); - - Widget _buildEmpty() => LayoutBuilder( - builder: (context, viewportConstraints) => SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Center( - child: Text(context.translations.noUsersLabel), - ), - ), - ), - ); - - Widget _buildListView( - List items, - ) { - final child = _isListView - ? ListView.separated( - physics: const AlwaysScrollableScrollPhysics(), - itemCount: items.isNotEmpty ? items.length + 1 : items.length, - separatorBuilder: (_, index) { - if (widget.separatorBuilder != null) { - return widget.separatorBuilder!(context, index); - } - return _separatorBuilder(context, index); - }, - itemBuilder: (context, index) => - _listItemBuilder(context, index, items), - ) - : GridView.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: widget.crossAxisCount, - ), - itemCount: items.isNotEmpty ? items.length + 1 : items.length, - physics: const AlwaysScrollableScrollPhysics(), - itemBuilder: (context, index) => - _gridItemBuilder(context, index, items), - ); - - return LazyLoadScrollView( - onEndOfPage: () => _userListController.paginateData!(), - child: child, - ); - } - - Widget _listItemBuilder(BuildContext context, int i, List items) { - final usersProvider = UsersBloc.of(context); - if (i < items.length) { - final item = items[i]; - return item.when( - headerItem: (header) { - final chatThemeData = StreamChatTheme.of(context); - return ColoredBox( - key: ValueKey('HEADER-$header'), - color: chatThemeData.colorTheme.textHighEmphasis.withOpacity(0.05), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), - child: Text( - header, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14.5, - color: chatThemeData.colorTheme.textLowEmphasis, - ), - ), - ), - ); - }, - userItem: (user) { - final selected = widget.selectedUsers?.contains(user) ?? false; - return Container( - key: ValueKey('USER-${user.id}'), - child: widget.userItemBuilder != null - ? widget.userItemBuilder!(context, user, selected) - : StreamUserItem( - user: user, - onTap: (user) => widget.onUserTap!(user, widget.userWidget), - onLongPress: widget.onUserLongPress, - onImageTap: widget.onImageTap, - selected: selected, - ), - ); - }, - ); - } else { - return _buildQueryProgressIndicator(context, usersProvider); - } - } - - Widget _gridItemBuilder(BuildContext context, int i, List items) { - final usersProvider = UsersBloc.of(context); - if (i < items.length) { - final item = items[i]; - return item.when( - headerItem: (_) => const Offstage(), - userItem: (user) { - final selected = widget.selectedUsers?.contains(user) ?? false; - return Container( - key: ValueKey('USER-${user.id}'), - child: widget.userItemBuilder != null - ? widget.userItemBuilder!(context, user, selected) - : Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - StreamUserAvatar( - user: user, - borderRadius: BorderRadius.circular(32), - selected: selected, - constraints: const BoxConstraints.tightFor( - height: 64, - width: 64, - ), - onlineIndicatorConstraints: - const BoxConstraints.tightFor( - height: 12, - width: 12, - ), - onTap: (user) => - widget.onUserTap!(user, widget.userWidget), - onLongPress: widget.onUserLongPress, - ), - const SizedBox(height: 4), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Text( - user.name, - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - ), - ], - ), - ); - }, - ); - } else { - return _buildQueryProgressIndicator(context, usersProvider); - } - } - - Widget _buildQueryProgressIndicator(context, UsersBlocState usersProvider) => - StreamBuilder( - stream: usersProvider.queryUsersLoading, - initialData: false, - builder: (context, snapshot) { - if (snapshot.hasError) { - return ColoredBox( - color: StreamChatTheme.of(context) - .colorTheme - .accentError - .withOpacity(0.2), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16), - child: Center( - child: Text(context.translations.loadingUsersError), - ), - ), - ); - } - return Container( - height: 100, - padding: const EdgeInsets.all(32), - child: Center( - child: snapshot.data! - ? const CircularProgressIndicator() - : Container(), - ), - ); - }, - ); - - Widget _separatorBuilder(context, i) => Container( - height: 1, - color: StreamChatTheme.of(context).colorTheme.borders, - ); -} diff --git a/packages/stream_chat_flutter/lib/src/utils/device_segmentation.dart b/packages/stream_chat_flutter/lib/src/utils/device_segmentation.dart new file mode 100644 index 00000000..bd65820c --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/utils/device_segmentation.dart @@ -0,0 +1,28 @@ +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// Returns true if the app is running on web. +bool get isWeb => CurrentPlatform.isWeb; + +/// Returns true if the app is running in a mobile device. +bool get isMobileDevice => CurrentPlatform.isIos || CurrentPlatform.isAndroid; + +/// Returns true if the app is running in a desktop device. +bool get isDesktopDevice => + CurrentPlatform.isMacOS || + CurrentPlatform.isWindows || + CurrentPlatform.isLinux; + +/// Returns true if the app is running on windows or linux platform. +bool get isDesktopVideoPlayerSupported => + // Dart VLC is not supported on MacOS. + !CurrentPlatform.isMacOS && + (CurrentPlatform.isWindows || CurrentPlatform.isLinux); + +/// Returns true if the app is running in a mobile or web. +bool get isMobileDeviceOrWeb => isWeb || isMobileDevice; + +/// Returns true if the app is running in a desktop or web. +bool get isDesktopDeviceOrWeb => isWeb || isDesktopDevice; + +/// Returns true if the app is running in a flutter test environment. +bool get isTestEnvironment => CurrentPlatform.isFlutterTest; diff --git a/packages/stream_chat_flutter/lib/src/extension.dart b/packages/stream_chat_flutter/lib/src/utils/extensions.dart similarity index 70% rename from packages/stream_chat_flutter/lib/src/extension.dart rename to packages/stream_chat_flutter/lib/src/utils/extensions.dart index c9566b0d..77faddca 100644 --- a/packages/stream_chat_flutter/lib/src/extension.dart +++ b/packages/stream_chat_flutter/lib/src/utils/extensions.dart @@ -2,12 +2,10 @@ import 'package:diacritic/diacritic.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/emoji/emoji.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:stream_chat_flutter/src/localization/translations.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -final _emojiChars = Emoji.chars(); - /// String extension extension StringExtension on String { /// Returns the capitalized string @@ -20,10 +18,15 @@ extension StringExtension on String { /// 1 to 3 emojis: big size with no text bubble. /// 4+ emojis or emojis+text: standard size with text bubble. bool get isOnlyEmoji { - if (isEmpty) return false; - if (length > 3) return false; - final characters = trim().characters; - return characters.every(_emojiChars.contains); + final trimmedString = trim(); + if (trimmedString.isEmpty) return false; + if (trimmedString.characters.length > 3) return false; + final emojiRegex = RegExp( + r'^(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])+$', + multiLine: true, + caseSensitive: false, + ); + return emojiRegex.hasMatch(trimmedString); } /// Removes accents and diacritics from the given String. @@ -40,7 +43,7 @@ extension StringExtension on String { // TODO: Are these sizes optimal? Consider web/desktop double width = 400, double height = 400, - String /*clip|crop|scale|fill*/ resize = 'scale', + String /*clip|crop|scale|fill*/ resize = 'clip', String /*center|top|bottom|left|right*/ crop = 'center', }) { final uri = Uri.parse(this); @@ -85,13 +88,73 @@ extension IterableX on Iterable { /// Useful extension for [PlatformFile] extension PlatformFileX on PlatformFile { /// Converts the [PlatformFile] into [AttachmentFile] - AttachmentFile get toAttachmentFile => AttachmentFile( - // ignore: avoid_redundant_argument_values - path: kIsWeb ? null : path, - name: name, - bytes: bytes, - size: size, - ); + AttachmentFile get toAttachmentFile { + return AttachmentFile( + path: kIsWeb ? null : path, + name: name, + bytes: bytes, + size: size, + ); + } + + /// Converts the [PlatformFile] to a [Attachment]. + Attachment toAttachment({required String type}) { + final file = toAttachmentFile; + final extraDataMap = {}; + + final mimeType = file.mimeType?.mimeType; + + if (mimeType != null) { + extraDataMap['mime_type'] = mimeType; + } + + extraDataMap['file_size'] = file.size!; + + final attachment = Attachment( + file: file, + type: type, + extraData: extraDataMap, + ); + + return attachment; + } +} + +/// Useful extension for [XFile] +extension XFileX on XFile { + /// Converts the [PlatformFile] into [AttachmentFile] + Future get toAttachmentFile async { + final bytes = await readAsBytes(); + return AttachmentFile( + name: name, + size: bytes.length, + path: path, + bytes: bytes, + ); + } + + /// Converts the [XFile] to a [Attachment]. + Future toAttachment({required String type}) async { + final file = await toAttachmentFile; + + final extraDataMap = {}; + + final mimeType = this.mimeType ?? file.mimeType?.mimeType; + + if (mimeType != null) { + extraDataMap['mime_type'] = mimeType; + } + + extraDataMap['file_size'] = file.size!; + + final attachment = Attachment( + file: file, + type: type, + extraData: extraDataMap, + ); + + return attachment; + } } /// Extension on [InputDecoration] @@ -196,28 +259,29 @@ extension IconButtonX on IconButton { bool? enableFeedback, BoxConstraints? constraints, Widget? icon, - }) => - IconButton( - iconSize: iconSize ?? this.iconSize, - visualDensity: visualDensity ?? this.visualDensity, - padding: padding ?? this.padding, - alignment: alignment ?? this.alignment, - splashRadius: splashRadius ?? this.splashRadius, - color: color ?? this.color, - focusColor: focusColor ?? this.focusColor, - hoverColor: hoverColor ?? this.hoverColor, - highlightColor: highlightColor ?? this.highlightColor, - splashColor: splashColor ?? this.splashColor, - disabledColor: disabledColor ?? this.disabledColor, - onPressed: onPressed ?? this.onPressed, - mouseCursor: mouseCursor ?? this.mouseCursor, - focusNode: focusNode ?? this.focusNode, - autofocus: autofocus ?? this.autofocus, - tooltip: tooltip ?? this.tooltip, - enableFeedback: enableFeedback ?? this.enableFeedback, - constraints: constraints ?? this.constraints, - icon: icon ?? this.icon, - ); + }) { + return IconButton( + iconSize: iconSize ?? this.iconSize, + visualDensity: visualDensity ?? this.visualDensity, + padding: padding ?? this.padding, + alignment: alignment ?? this.alignment, + splashRadius: splashRadius ?? this.splashRadius, + color: color ?? this.color, + focusColor: focusColor ?? this.focusColor, + hoverColor: hoverColor ?? this.hoverColor, + highlightColor: highlightColor ?? this.highlightColor, + splashColor: splashColor ?? this.splashColor, + disabledColor: disabledColor ?? this.disabledColor, + onPressed: onPressed ?? this.onPressed, + mouseCursor: mouseCursor ?? this.mouseCursor, + focusNode: focusNode ?? this.focusNode, + autofocus: autofocus ?? this.autofocus, + tooltip: tooltip ?? this.tooltip, + enableFeedback: enableFeedback ?? this.enableFeedback, + constraints: constraints ?? this.constraints, + icon: icon ?? this.icon, + ); + } } /// Extensions on List @@ -328,3 +392,47 @@ extension TypeX on T? { /// otherwise returns false. bool isNullOrMatches(T value) => this == null || this == value; } + +/// Useful extensions on [FileType] +extension FileTypeX on FileType { + /// Converts the [FileType] to a [String]. + String toAttachmentType() { + switch (this) { + case FileType.image: + return 'image'; + case FileType.video: + return 'video'; + case FileType.audio: + return 'audio'; + case FileType.any: + case FileType.media: + case FileType.custom: + return 'file'; + } + } +} + +/// Useful extensions on [AttachmentPickerType] +extension AttachmentPickerTypeX on AttachmentPickerType { + /// Converts the [AttachmentPickerType] to a [FileType]. + FileType get fileType { + switch (this) { + case AttachmentPickerType.images: + return FileType.image; + case AttachmentPickerType.videos: + return FileType.video; + case AttachmentPickerType.files: + return FileType.any; + case AttachmentPickerType.audios: + return FileType.audio; + } + } +} + +/// Useful extensions on [StreamSvgIcon]. +extension StreamSvgIconX on StreamSvgIcon { + /// Converts the [StreamSvgIcon] to a [StreamIconThemeSvgIcon]. + StreamIconThemeSvgIcon toIconThemeSvgIcon() { + return StreamIconThemeSvgIcon.fromStreamSvgIcon(this); + } +} diff --git a/packages/stream_chat_flutter/lib/src/utils.dart b/packages/stream_chat_flutter/lib/src/utils/helpers.dart similarity index 80% rename from packages/stream_chat_flutter/lib/src/utils.dart rename to packages/stream_chat_flutter/lib/src/utils/helpers.dart index 04800e98..718160a2 100644 --- a/packages/stream_chat_flutter/lib/src/utils.dart +++ b/packages/stream_chat_flutter/lib/src/utils/helpers.dart @@ -2,10 +2,28 @@ import 'dart:async'; import 'dart:math' as math; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:flutter_portal/flutter_portal.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:synchronized/synchronized.dart'; import 'package:url_launcher/url_launcher.dart'; +final _permissionRequestLock = Lock(); + +/// Executes [computation] when [_permissionRequestLock] is available. +/// +/// Only one asynchronous block can run while the [_permissionRequestLock] +/// is retained. +Future runInPermissionRequestLock( + FutureOr Function() computation, { + Duration? timeout, +}) { + return _permissionRequestLock.synchronized( + computation, + timeout: timeout, + ); +} + /// Launch URL Future launchURL(BuildContext context, String url) async { try { @@ -43,6 +61,11 @@ bool getEffectiveCenterTitle( } /// Shows confirmation dialog +@Deprecated( + ''' + showConfirmationDialog is deprecated. + Use showConfirmationBottomSheet instead.''', +) Future showConfirmationDialog( BuildContext context, { required String title, @@ -50,6 +73,24 @@ Future showConfirmationDialog( Widget? icon, String? question, String? cancelText, +}) => + showConfirmationBottomSheet( + context, + title: title, + okText: okText, + icon: icon, + question: question, + cancelText: cancelText, + ); + +/// Shows confirmation bottom sheet +Future showConfirmationBottomSheet( + BuildContext context, { + required String title, + required String okText, + Widget? icon, + String? question, + String? cancelText, }) { final chatThemeData = StreamChatTheme.of(context); return showModalBottomSheet( @@ -110,9 +151,7 @@ Future showConfirmationDialog( child: Container( alignment: Alignment.center, child: TextButton( - onPressed: () { - Navigator.pop(context, true); - }, + onPressed: () => Navigator.of(context).pop(true), child: Text( okText, style: chatThemeData.textTheme.bodyBold.copyWith( @@ -132,6 +171,11 @@ Future showConfirmationDialog( } /// Shows info dialog +@Deprecated( + ''' + showInfoDialog is deprecated. + Use showInfoBottomSheet instead.''', +) Future showInfoDialog( BuildContext context, { required String title, @@ -139,6 +183,24 @@ Future showInfoDialog( Widget? icon, String? details, StreamChatThemeData? theme, +}) => + showInfoBottomSheet( + context, + title: title, + okText: okText, + icon: icon, + details: details, + theme: theme, + ); + +/// Shows info bottom sheet +Future showInfoBottomSheet( + BuildContext context, { + required String title, + required String okText, + Widget? icon, + String? details, + StreamChatThemeData? theme, }) { final chatThemeData = StreamChatTheme.of(context); return showModalBottomSheet( @@ -315,8 +377,9 @@ String fileSize(dynamic size, [int round = 2]) { } /// -StreamSvgIcon getFileTypeImage(String? type) { - switch (type) { +StreamSvgIcon getFileTypeImage(String? mimeType) { + final subtype = mimeType?.split('/').last; + switch (subtype) { case '7z': return StreamSvgIcon.filetype7z(); case 'csv': @@ -357,6 +420,12 @@ StreamSvgIcon getFileTypeImage(String? type) { } /// Wraps attachment widget with custom shape +@Deprecated( + ''' +wrapAttachmentWidget is deprecated. +Use WrapAttachmentWidget instead +''', +) Widget wrapAttachmentWidget( BuildContext context, Widget attachmentWidget, @@ -364,59 +433,40 @@ Widget wrapAttachmentWidget( // ignore: avoid_positional_boolean_parameters bool reverse, ) => - Material( + WrapAttachmentWidget( + attachmentWidget: attachmentWidget, + attachmentShape: attachmentShape, + reverse: reverse, + ); + +/// Wraps attachment widget with custom shape +class WrapAttachmentWidget extends StatelessWidget { + /// Builds a [WrapAttachmentWidget]. + const WrapAttachmentWidget({ + super.key, + required this.attachmentWidget, + required this.attachmentShape, + required this.reverse, + }); + + /// The widget to wrap + final Widget attachmentWidget; + + /// The shape of the wrapper + final ShapeBorder attachmentShape; + + /// Whether to reverse the wrapper shape + final bool reverse; + + @override + Widget build(BuildContext context) { + return Material( clipBehavior: Clip.hardEdge, shape: attachmentShape, type: MaterialType.transparency, child: attachmentWidget, ); - -/// Represents a 2-tuple, or pair. -class Tuple2 { - /// Creates a new tuple value with the specified items. - const Tuple2(this.item1, this.item2); - - /// Create a new tuple value with the specified list [items]. - factory Tuple2.fromList(List items) { - if (items.length != 2) { - throw ArgumentError('items must have length 2'); - } - - return Tuple2(items[0] as T1, items[1] as T2); } - - /// Returns the first item of the tuple - final T1 item1; - - /// Returns the second item of the tuple - final T2 item2; - - /// Returns a tuple with the first item set to the specified value. - Tuple2 withItem1(T1 v) => Tuple2(v, item2); - - /// Returns a tuple with the second item set to the specified value. - Tuple2 withItem2(T2 v) => Tuple2(item1, v); - - /// Creates a [List] containing the items of this [Tuple2]. - /// - /// The elements are in item order. The list is variable-length - /// if [growable] is true. - List toList({bool growable = false}) => - List.from([item1, item2], growable: growable); - - @override - String toString() => '[$item1, $item2]'; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Tuple2 && - runtimeType == other.runtimeType && - item1 == other.item1 && - item2 == other.item2; - - @override - int get hashCode => item1.hashCode ^ item2.hashCode; } /// Levenshtein algorithm implementation based on: @@ -457,8 +507,8 @@ int levenshtein(String s, String t, {bool caseSensitive = true}) { /// An easy way to handle attachment related operations on a message extension AttachmentPackagesX on Message { - /// This extension will return a List of type [StreamAttachmentPackage] - /// from the existing attachments of the message + /// This extension will return a List of type [StreamAttachmentPackage] from + /// the existing attachments of the message List getAttachmentPackageList() { final _attachmentPackages = List.generate( attachments.length, @@ -470,3 +520,13 @@ extension AttachmentPackagesX on Message { return _attachmentPackages; } } + +/// PortalLabel that refers to [StreamMessageListView] +const kPortalMessageListViewLabel = _PortalMessageListViewLabel(); + +class _PortalMessageListViewLabel extends PortalLabel { + const _PortalMessageListViewLabel() : super(null); + + @override + String toString() => 'PortalLabel.MessageWidget'; +} diff --git a/packages/stream_chat_flutter/lib/src/utils/typedefs.dart b/packages/stream_chat_flutter/lib/src/utils/typedefs.dart new file mode 100644 index 00000000..b6e1af43 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/utils/typedefs.dart @@ -0,0 +1,360 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/message_input/attachment_button.dart'; +import 'package:stream_chat_flutter/src/message_input/command_button.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template inProgressBuilder} +/// A widget builder for representing in-progress attachment uploads. +/// {@endtemplate} +typedef InProgressBuilder = Widget Function(BuildContext, int, int); + +/// {@template failedBuilder} +/// A widget builder for representing failed attachment uploads. +/// {@endtemplate} +typedef FailedBuilder = Widget Function(BuildContext, String); + +/// {@template successBuilder} +/// A widget builder for representing successful attachment uploads. +/// {@endtemplate} +typedef SuccessBuilder = WidgetBuilder; + +/// {@template preparingBuilder} +/// A widget builder for representing pre-upload attachment state. +/// {@endtemplate} +typedef PreparingBuilder = WidgetBuilder; + +/// {@template onAttachmentTap} +/// The action to perform when the attachment is tapped or clicked. +/// {@endtemplate} +typedef OnAttachmentTap = VoidCallback; + +/// {@template showMessageCallback} +/// The action to perform when "show message" is tapped +/// {@endtemplate} +typedef ShowMessageCallback = void Function(Message message, Channel channel); + +/// {@template showMessageCallback} +/// The action to perform when "reply message" is tapped +/// {@endtemplate} +typedef ReplyMessageCallback = void Function(Message message); + +/// {@template onImageGroupAttachmentTap} +/// The action to perform when a specific image attachment in an [ImageGroup] +/// is tapped or clicked. +/// {@endtemplate} +typedef OnImageGroupAttachmentTap = void Function( + Message message, + Attachment attachment, +); + +/// {@template onUserAvatarPress} +/// The action to perform when a user's avatar is tapped, clicked, or +/// long-pressed. +/// {@endtemplate} +typedef OnUserAvatarPress = void Function(User); + +/// {@template placeholderUserImage} +/// A widget builder that will build placeholder user images while loading a +/// user image. +/// {@endtemplate} +typedef PlaceholderUserImage = Widget Function(BuildContext, User); + +/// {@template editMessageInputBuilder} +// ignore: deprecated_member_use_from_same_package +/// A widget builder for building a pre-populated [MessageInput] for use in +/// editing messages. +/// {@endtemplate} +typedef EditMessageInputBuilder = Widget Function(BuildContext, Message); + +/// {@template channelListHeaderTitleBuilder} +// ignore: deprecated_member_use_from_same_package +/// A widget builder for custom [ChannelListHeader] title widgets. +/// {@endtemplate} +typedef ChannelListHeaderTitleBuilder = Widget Function( + BuildContext context, + ConnectionStatus status, + StreamChatClient client, +); + +/// {@template channelTapCallback} +/// The action to perform when a channel is tapped or clicked. +/// {@endtemplate} +typedef ChannelTapCallback = void Function(Channel, Widget?); + +/// {@template channelInfoCallback} +/// The action to perform when a particular channel slidable option is tapped +/// or clicked. +/// {@endtemplate} +typedef ChannelInfoCallback = void Function(Channel); + +/// {@template channelPreviewBuilder} +/// Builder used to create a custom [ChannelPreview] for a [Channel] +/// {@endtemplate} +typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel); + +/// {@template viewInfoCallback} +/// Callback for when 'View Info' is tapped +/// {@endtemplate} +typedef ViewInfoCallback = void Function(Channel); + +/// {@template attachmentActionsBuilder} +/// A widget builder for representing the actions a user can take on an +/// attachment. +/// +/// [defaultActionsModal] is the default [AttachmentActionsModal] configuration. +/// Use [defaultActionsModal.copyWith] to easily customize it +/// {@endtemplate} +typedef AttachmentActionsBuilder = Widget Function( + BuildContext context, + Attachment attachment, + AttachmentActionsModal defaultActionsModal, +); + +/// {@template errorListener} +/// A callback that can be passed to [StreamMessageInput.onError]. +/// +/// This callback should not throw. +/// +/// It exists merely for error reporting, and should not be used otherwise. +/// {@endtemplate} +typedef ErrorListener = void Function( + Object error, + StackTrace? stackTrace, +); + +/// {@template attachmentLimitExceededListener} +/// A callback that can be passed to +/// [StreamMessageInput.onAttachmentLimitExceed]. +/// +/// This callback should not throw. +/// +/// It exists merely for showing custom error, and should not be used otherwise. +/// {@endtemplate} +typedef AttachmentLimitExceedListener = void Function( + int limit, + String error, +); + +/// {@template attachmentThumbnailBuilder} +/// A widget builder for representing attachment thumbnails. +/// {@endtemplate} +typedef AttachmentThumbnailBuilder = Widget Function( + BuildContext, + Attachment, +); + +/// {@macro mentionTileBuilder} +/// A widget builder for representing a custom mention tile. +/// {@endtemplate} +typedef MentionTileBuilder = Widget Function( + BuildContext context, + Member member, +); + +/// {@template mentionTileOverlayBuilder} +/// A widget builder for representing a custom mention tile within a +/// [UserMentionsOverlay]. +/// {@endtemplate} +typedef MentionTileOverlayBuilder = Widget Function( + BuildContext context, + User user, +); + +/// {@template userMentionTileBuilder} +/// A builder function for representing a custom user mention tile. +/// +// ignore: deprecated_member_use_from_same_package +/// Use [UserMentionTile] for the default implementation. +/// {@endtemplate} +typedef UserMentionTileBuilder = Widget Function( + BuildContext context, + User user, +); + +/// {@template actionButtonBuilder} +/// A widget builder for building a custom command button. +/// +/// [commandButton] is the default [CommandButton] configuration, +/// use [commandButton.copyWith] to easily customize it. +/// {@endtemplate} +typedef CommandButtonBuilder = Widget Function( + BuildContext context, + CommandButton commandButton, +); + +/// {@template actionButtonBuilder} +/// A widget builder for building a custom action button. +/// +/// [attachmentButton] is the default [AttachmentButton] configuration, +/// use [attachmentButton.copyWith] to easily customize it. +/// {@endtemplate} +typedef AttachmentButtonBuilder = Widget Function( + BuildContext context, + AttachmentButton attachmentButton, +); + +/// {@template quotedMessageAttachmentThumbnailBuilder} +/// A widget builder for building a custom quoted message attachment thumbnail. +/// {@endtemplate} +typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function( + BuildContext, + Attachment, +); + +/// {@template onMessageWidgetAttachmentTap} +/// The action to perform when an attachment in an [StreamMessageWidget] +/// is tapped or clicked. +/// {@endtemplate} +typedef OnMessageWidgetAttachmentTap = void Function( + Message message, + Attachment attachment, +); + +/// {@template attachmentBuilder} +/// A widget builder for representing attachments. +/// {@endtemplate} +typedef AttachmentBuilder = Widget Function( + BuildContext, + Message, + List, +); + +/// {@template onQuotedMessageTap} +/// The action to perform when a quoted message is tapped. +/// {@endtemplate} +typedef OnQuotedMessageTap = void Function(String?); + +/// {@template onMessageTap} +/// The action to perform when a message is tapped. +/// {@endtemplate} +typedef OnMessageTap = void Function(Message); + +/// {@template messageSearchItemTapCallback} +/// The action to perform when tapping or clicking on a user in a +// ignore: deprecated_member_use_from_same_package +/// [MessageSearchListView]. +/// {@endtemplate} +typedef MessageSearchItemTapCallback = void Function(GetMessageResponse); + +/// {@template messageSearchItemBuilder} +/// A widget builder used to create a custom [ListUserItem] from a [User]. +/// {@endtemplate} +typedef MessageSearchItemBuilder = Widget Function( + BuildContext, + GetMessageResponse, +); + +/// {@template messageBuilder} +/// A widget builder for creating custom message UI. +/// +/// [defaultMessageWidget] is the default [StreamMessageWidget] configuration. +/// Use [defaultMessageWidget.copyWith] to customize it. +/// {@endtemplate} +typedef MessageBuilder = Widget Function( + BuildContext, + MessageDetails, + List, + StreamMessageWidget defaultMessageWidget, +); + +/// {@template parentMessageBuilder} +/// A widget builder for creating custom parent message UI. +/// +/// [defaultMessageWidget] is the default [StreamMessageWidget] configuration. +/// Use [defaultMessageWidget.copyWith] to customize it. +/// {@endtemplate} +typedef ParentMessageBuilder = Widget Function( + BuildContext, + Message?, + StreamMessageWidget defaultMessageWidget, +); + +/// {@template systemMessageBuilder} +/// A widget builder for creating custom system messages. +/// {@endtemplate} +typedef SystemMessageBuilder = Widget Function( + BuildContext, + Message, +); + +/// {@template threadBuilder} +/// A widget builder for creating custom thread UI. +/// {@endtemplate} +typedef ThreadBuilder = Widget Function(BuildContext context, Message? parent); + +/// {@template threadTapCallback} +/// The action to perform when threads are tapped. +/// {@endtemplate} +typedef ThreadTapCallback = void Function(Message, Widget?); + +/// {@template onMessageSwiped} +/// The action to perform when a message is swiped. +/// {@endtemplate} +typedef OnMessageSwiped = void Function(Message); + +/// {@template spacingWidgetBuilder} +/// A widget builder for creating certain spacing after widgets. +/// +/// This spacing can be in the form of any widgets you like. +/// +/// A List of [SpacingType] is provided to help inform the decision of +/// what to build after the message (thread, difference in time between +/// current and last message, default spacing, etc). +/// +/// Example: +/// ```dart +/// MessageListView( +/// spacingWidgetBuilder: (context, list) { +/// if(list.contains(SpacingType.defaultSpacing)) { +/// return SizedBox(height: 2.0,); +/// } else { +/// return SizedBox(height: 8.0,); +/// } +/// }, +/// ), +/// ```dart +/// {@endtemplate} +typedef SpacingWidgetBuilder = Widget Function( + BuildContext context, + List spacingTypes, +); + +/// {@template attachmentDownloader} +/// A callback for downloading an attachment asset. +/// {@endtemplate} +/// Callback to download an attachment asset +typedef AttachmentDownloader = Future Function( + Attachment attachment, { + ProgressCallback? onReceiveProgress, + Map? queryParameters, + CancelToken? cancelToken, + bool deleteOnError, + Options? options, +}); + +/// Callback to receive the path once the attachment asset is downloaded +typedef DownloadedPathCallback = void Function(String? path); + +/// {@template userTapCallback} +/// Callback called when tapping on a user +/// {@endtemplate} +typedef UserTapCallback = void Function(User, Widget?); + +/// {@template userItemBuilder} +/// Builder used to create a custom [ListUserItem] from a [User] +/// {@endtemplate} +typedef UserItemBuilder = Widget Function(BuildContext, User, bool); + +/// The action to perform when the "scroll to bottom" button is pressed +/// within a [MessageListView]. +typedef OnScrollToBottom = Function(int unreadCount); + +/// Widget builder for widgets that may require data from the +/// [MessageInputController]. +typedef MessageRelatedBuilder = Widget Function( + BuildContext context, + StreamMessageInputController messageInputController, +); + +/// A function that returns true if the message is valid and can be sent. +typedef MessageValidator = bool Function(Message message); diff --git a/packages/stream_chat_flutter/lib/src/utils/utils.dart b/packages/stream_chat_flutter/lib/src/utils/utils.dart new file mode 100644 index 00000000..34eb6503 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/utils/utils.dart @@ -0,0 +1,4 @@ +export 'device_segmentation.dart'; +export 'extensions.dart'; +export 'helpers.dart'; +export 'typedefs.dart'; diff --git a/packages/stream_chat_flutter/lib/src/v4/message_input/countdown_button.dart b/packages/stream_chat_flutter/lib/src/v4/message_input/countdown_button.dart deleted file mode 100644 index 75416790..00000000 --- a/packages/stream_chat_flutter/lib/src/v4/message_input/countdown_button.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// Button for showing visual component of slow mode. -class StreamCountdownButton extends StatelessWidget { - /// Constructor for creating [StreamCountdownButton]. - const StreamCountdownButton({ - super.key, - required this.count, - }); - - /// Count of time remaining to show to the user. - final int count; - - @override - Widget build(BuildContext context) => Padding( - padding: const EdgeInsets.all(8), - child: DecoratedBox( - decoration: BoxDecoration( - color: StreamChatTheme.of(context).colorTheme.disabled, - shape: BoxShape.circle, - ), - child: SizedBox( - height: 24, - width: 24, - child: Center( - child: Text('$count'), - ), - ), - ), - ); -} diff --git a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart b/packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart deleted file mode 100644 index 8c1e14f8..00000000 --- a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart +++ /dev/null @@ -1,642 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:math' as math; - -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:photo_manager/photo_manager.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/media_list_view.dart'; -import 'package:stream_chat_flutter/src/media_list_view_controller.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// Callback for when a file has to be picked. -typedef FilePickerCallback = void Function( - DefaultAttachmentTypes fileType, { - bool camera, -}); - -/// Callback for building an icon for a custom attachment type. -typedef CustomAttachmentIconBuilder = Widget Function( - BuildContext context, - bool active, -); - -/// A widget that allows to pick an attachment. -class StreamAttachmentPicker extends StatefulWidget { - /// Default constructor for [StreamAttachmentPicker] which creates the Stream - /// attachment picker widget. - const StreamAttachmentPicker({ - super.key, - required this.messageInputController, - required this.onFilePicked, - this.isOpen = false, - this.pickerSize = 360.0, - this.attachmentLimit = 10, - this.onAttachmentLimitExceeded, - this.maxAttachmentSize = kDefaultMaxAttachmentSize, - this.onError, - this.allowedAttachmentTypes = const [ - DefaultAttachmentTypes.image, - DefaultAttachmentTypes.file, - DefaultAttachmentTypes.video, - ], - this.customAttachmentTypes = const [], - this.attachmentThumbnailSize = const ThumbnailSize(400, 400), - this.attachmentThumbnailFormat = ThumbnailFormat.jpeg, - this.attachmentThumbnailQuality = 100, - this.attachmentThumbnailScale = 1, - }); - - /// True if the picker is open. - final bool isOpen; - - /// The picker size in height. - final double pickerSize; - - /// The [StreamMessageInputController] linked to this picker. - final StreamMessageInputController messageInputController; - - /// The limit of attachments that can be picked. - final int attachmentLimit; - - /// The callback for when the attachment limit is exceeded. - final AttachmentLimitExceedListener? onAttachmentLimitExceeded; - - /// Callback for when an error occurs in the attachment picker. - final ValueChanged? onError; - - /// Callback for when file is picked. - final FilePickerCallback onFilePicked; - - /// Max attachment size in bytes: - /// - Defaults to 20 MB - /// - Do not set it if you're using our default CDN - final int maxAttachmentSize; - - /// The list of attachment types that can be picked. - final List allowedAttachmentTypes; - - /// The list of custom attachment types that can be picked. - final List customAttachmentTypes; - - /// Size of the attachment thumbnails. - /// - /// Defaults to (400, 400). - final ThumbnailSize attachmentThumbnailSize; - - /// Format of the attachment thumbnails. - /// - /// Defaults to [ThumbnailFormat.jpeg]. - final ThumbnailFormat attachmentThumbnailFormat; - - /// The quality value for the attachment thumbnails. - /// - /// Valid from 1 to 100. - /// Defaults to 100. - final int attachmentThumbnailQuality; - - /// The scale to apply on the [attachmentThumbnailSize]. - final double attachmentThumbnailScale; - - /// Used to create a new copy of [StreamAttachmentPicker] with modified - /// properties. - StreamAttachmentPicker copyWith({ - Key? key, - StreamMessageInputController? messageInputController, - FilePickerCallback? onFilePicked, - bool? isOpen, - double? pickerSize, - int? attachmentLimit, - AttachmentLimitExceedListener? onAttachmentLimitExceeded, - int? maxAttachmentSize, - ValueChanged? onChangeInputState, - ValueChanged? onError, - List? allowedAttachmentTypes, - List? customAttachmentTypes, - ThumbnailSize? attachmentThumbnailSize, - ThumbnailFormat? attachmentThumbnailFormat, - int? attachmentThumbnailQuality, - double? attachmentThumbnailScale, - }) => - StreamAttachmentPicker( - key: key ?? this.key, - messageInputController: - messageInputController ?? this.messageInputController, - onFilePicked: onFilePicked ?? this.onFilePicked, - isOpen: isOpen ?? this.isOpen, - pickerSize: pickerSize ?? this.pickerSize, - attachmentLimit: attachmentLimit ?? this.attachmentLimit, - onAttachmentLimitExceeded: - onAttachmentLimitExceeded ?? this.onAttachmentLimitExceeded, - maxAttachmentSize: maxAttachmentSize ?? this.maxAttachmentSize, - onError: onError ?? this.onError, - allowedAttachmentTypes: - allowedAttachmentTypes ?? this.allowedAttachmentTypes, - customAttachmentTypes: - customAttachmentTypes ?? this.customAttachmentTypes, - attachmentThumbnailSize: - attachmentThumbnailSize ?? this.attachmentThumbnailSize, - attachmentThumbnailFormat: - attachmentThumbnailFormat ?? this.attachmentThumbnailFormat, - attachmentThumbnailQuality: - attachmentThumbnailQuality ?? this.attachmentThumbnailQuality, - attachmentThumbnailScale: - attachmentThumbnailScale ?? this.attachmentThumbnailScale, - ); - - @override - State createState() => _StreamAttachmentPickerState(); -} - -class _StreamAttachmentPickerState extends State { - int _filePickerIndex = 0; - final _mediaListViewController = MediaListViewController(); - - @override - Widget build(BuildContext context) { - final _streamChatTheme = StreamChatTheme.of(context); - final messageInputController = widget.messageInputController; - - final _attachmentContainsImage = - messageInputController.attachments.any((it) => it.type == 'image'); - - final _attachmentContainsFile = - messageInputController.attachments.any((it) => it.type == 'file'); - - final _attachmentContainsVideo = - messageInputController.attachments.any((it) => it.type == 'video'); - - final attachmentLimitCrossed = - messageInputController.attachments.length >= widget.attachmentLimit; - - Color _getIconColor(int index) { - final streamChatThemeData = _streamChatTheme; - switch (index) { - case 0: - return _filePickerIndex == 0 || _attachmentContainsImage - ? streamChatThemeData.colorTheme.accentPrimary - : (_attachmentContainsImage - ? streamChatThemeData.colorTheme.accentPrimary - : streamChatThemeData.colorTheme.textHighEmphasis.withOpacity( - messageInputController.attachments.isEmpty ? 0.5 : 0.2, - )); - case 1: - return _attachmentContainsFile - ? streamChatThemeData.colorTheme.accentPrimary - : (messageInputController.attachments.isEmpty - ? streamChatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.5) - : streamChatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.2)); - case 2: - return widget.messageInputController.attachments.isNotEmpty && - (!_attachmentContainsImage || attachmentLimitCrossed) - ? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2) - : _attachmentContainsFile && - messageInputController.attachments.isNotEmpty - ? streamChatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.2) - : streamChatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.5); - case 3: - return widget.messageInputController.attachments.isNotEmpty && - (!_attachmentContainsVideo || attachmentLimitCrossed) - ? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2) - : _attachmentContainsFile && - messageInputController.attachments.isNotEmpty - ? streamChatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.2) - : streamChatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.5); - default: - return Colors.black; - } - } - - return AnimatedContainer( - duration: - widget.isOpen ? const Duration(milliseconds: 300) : Duration.zero, - curve: Curves.easeOut, - height: widget.isOpen ? widget.pickerSize : 0, - child: SingleChildScrollView( - child: SizedBox( - height: widget.pickerSize, - child: Material( - color: _streamChatTheme.colorTheme.inputBg, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - if (widget.allowedAttachmentTypes - .contains(DefaultAttachmentTypes.image)) - IconButton( - icon: StreamSvgIcon.pictures( - color: _getIconColor(0), - ), - onPressed: - messageInputController.attachments.isNotEmpty && - !_attachmentContainsImage - ? null - : () { - setState(() { - _filePickerIndex = 0; - }); - }, - ), - if (widget.allowedAttachmentTypes - .contains(DefaultAttachmentTypes.file)) - IconButton( - iconSize: 32, - icon: StreamSvgIcon.files( - color: _getIconColor(1), - ), - onPressed: messageInputController - .attachments.isNotEmpty && - !_attachmentContainsFile - ? null - : () { - widget - .onFilePicked(DefaultAttachmentTypes.file); - }, - ), - if (widget.allowedAttachmentTypes - .contains(DefaultAttachmentTypes.image)) - IconButton( - icon: StreamSvgIcon.camera( - color: _getIconColor(2), - ), - onPressed: attachmentLimitCrossed || - (messageInputController - .attachments.isNotEmpty && - !_attachmentContainsVideo) - ? null - : () { - widget.onFilePicked( - DefaultAttachmentTypes.image, - camera: true, - ); - }, - ), - if (widget.allowedAttachmentTypes - .contains(DefaultAttachmentTypes.video)) - IconButton( - padding: EdgeInsets.zero, - icon: StreamSvgIcon.record( - color: _getIconColor(3), - ), - onPressed: attachmentLimitCrossed || - (messageInputController - .attachments.isNotEmpty && - !_attachmentContainsVideo) - ? null - : () { - widget.onFilePicked( - DefaultAttachmentTypes.video, - camera: true, - ); - }, - ), - for (int i = 0; - i < widget.customAttachmentTypes.length; - i++) - IconButton( - onPressed: () { - if (messageInputController.attachments.isNotEmpty) { - if (!messageInputController.attachments.any((e) => - e.type == - widget.customAttachmentTypes[i].type)) { - return; - } - } - - setState(() { - _filePickerIndex = i + 1; - }); - }, - icon: widget.customAttachmentTypes[i] - .iconBuilder(context, _filePickerIndex == i + 1), - ), - const Spacer(), - if (widget.isOpen) - FutureBuilder( - future: PhotoManager.requestPermissionExtend(), - builder: (context, snapshot) { - if (snapshot.hasData && - snapshot.data == PermissionState.limited) { - return TextButton( - child: Text(context.translations.viewLibrary), - onPressed: () async { - await PhotoManager.presentLimited(); - _mediaListViewController.updateMedia( - newValue: true, - ); - }, - ); - } - - return const SizedBox.shrink(); - }, - ), - ], - ), - DecoratedBox( - decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.barsBg, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - ), - child: Center( - child: Padding( - padding: const EdgeInsets.all(8), - child: Container( - width: 40, - height: 4, - decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.inputBg, - borderRadius: BorderRadius.circular(4), - ), - ), - ), - ), - ), - if (widget.isOpen && - (widget.allowedAttachmentTypes - .contains(DefaultAttachmentTypes.image) || - (widget.allowedAttachmentTypes - .contains(DefaultAttachmentTypes.file)))) - Expanded( - child: DecoratedBox( - decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.barsBg, - borderRadius: BorderRadius.circular(8), - ), - child: _PickerWidget( - mediaListViewController: _mediaListViewController, - filePickerIndex: _filePickerIndex, - streamChatTheme: _streamChatTheme, - containsFile: _attachmentContainsFile, - selectedMedias: messageInputController.attachments - .map((e) => e.id) - .toList(), - onAddMoreFilesClick: widget.onFilePicked, - onMediaSelected: (media) { - if (messageInputController.attachments - .any((e) => e.id == media.id)) { - messageInputController - .removeAttachmentById(media.id); - } else { - _addAssetAttachment(media); - } - }, - allowedAttachmentTypes: widget.allowedAttachmentTypes, - customAttachmentTypes: widget.customAttachmentTypes, - mediaThumbnailSize: widget.attachmentThumbnailSize, - mediaThumbnailFormat: widget.attachmentThumbnailFormat, - mediaThumbnailQuality: - widget.attachmentThumbnailQuality, - mediaThumbnailScale: widget.attachmentThumbnailScale, - ), - ), - ), - ], - ), - ), - ), - ), - ); - } - - void _addAssetAttachment(AssetEntity medium) async { - final mediaFile = await medium.originFile; - if (mediaFile == null) return; - - final tempDir = await getTemporaryDirectory(); - - // TODO: Confirm that this max resolution is final - // Taken from https://getstream.io/chat/docs/flutter-dart/file_uploads/?language=dart#image-resizing - const maxCDNImageResolution = 16800000; - final imageResolution = medium.width * medium.height; - File? cachedFile; - if (imageResolution > maxCDNImageResolution) { - final aspect = imageResolution / maxCDNImageResolution; - final updatedSize = medium.size / (math.sqrt(aspect)); - final resizedImage = await medium.thumbnailDataWithSize( - ThumbnailSize( - updatedSize.width.floor(), - updatedSize.height.floor(), - ), - quality: 70, // TODO: investigate compressing all images - ); - final file = - await File('${tempDir.path}/${mediaFile.path.split('/').last}') - .create(); - file.writeAsBytesSync(resizedImage!); - cachedFile = file; - } else { - cachedFile = await mediaFile - .copy('${tempDir.path}/${mediaFile.path.split('/').last}'); - } - - final file = AttachmentFile( - path: cachedFile.path, - size: await cachedFile.length(), - bytes: cachedFile.readAsBytesSync(), - ); - - if (file.size! > widget.maxAttachmentSize) { - return widget.onError?.call( - context.translations.fileTooLargeError( - widget.maxAttachmentSize / (1024 * 1024), - ), - ); - } - - setState(() { - final attachment = Attachment( - id: medium.id, - file: file, - type: medium.type == AssetType.image ? 'image' : 'video', - ); - _addAttachments([attachment]); - }); - } - - /// Adds an attachment to the [messageInputController.attachments] map - void _addAttachments(Iterable attachments) { - final limit = widget.attachmentLimit; - final length = - widget.messageInputController.attachments.length + attachments.length; - if (length > limit) { - final onAttachmentLimitExceed = widget.onAttachmentLimitExceeded; - if (onAttachmentLimitExceed != null) { - return onAttachmentLimitExceed( - widget.attachmentLimit, - context.translations.attachmentLimitExceedError(limit), - ); - } - return widget.onError?.call( - context.translations.attachmentLimitExceedError(limit), - ); - } - for (final attachment in attachments) { - widget.messageInputController.addAttachment(attachment); - } - } -} - -class _PickerWidget extends StatefulWidget { - const _PickerWidget({ - required this.filePickerIndex, - required this.containsFile, - required this.selectedMedias, - required this.onAddMoreFilesClick, - required this.onMediaSelected, - required this.streamChatTheme, - required this.allowedAttachmentTypes, - required this.customAttachmentTypes, - required this.mediaListViewController, - this.mediaThumbnailSize = const ThumbnailSize(400, 400), - this.mediaThumbnailFormat = ThumbnailFormat.jpeg, - this.mediaThumbnailQuality = 100, - this.mediaThumbnailScale = 1, - }); - - final int filePickerIndex; - final bool containsFile; - final List selectedMedias; - final void Function(DefaultAttachmentTypes) onAddMoreFilesClick; - final void Function(AssetEntity) onMediaSelected; - final StreamChatThemeData streamChatTheme; - final List allowedAttachmentTypes; - final List customAttachmentTypes; - final MediaListViewController mediaListViewController; - final ThumbnailSize mediaThumbnailSize; - final ThumbnailFormat mediaThumbnailFormat; - final int mediaThumbnailQuality; - final double mediaThumbnailScale; - - @override - _PickerWidgetState createState() => _PickerWidgetState(); -} - -class _PickerWidgetState extends State<_PickerWidget> { - Future? requestPermission; - - @override - void initState() { - super.initState(); - requestPermission = PhotoManager.requestPermissionExtend(); - } - - @override - Widget build(BuildContext context) { - if (widget.filePickerIndex != 0) { - return widget.customAttachmentTypes[widget.filePickerIndex - 1] - .pickerBuilder(context); - } - return FutureBuilder( - future: requestPermission, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Offstage(); - } - - if ([PermissionState.authorized, PermissionState.limited] - .contains(snapshot.data)) { - if (widget.containsFile || - !widget.allowedAttachmentTypes - .contains(DefaultAttachmentTypes.image)) { - return GestureDetector( - onTap: () { - widget.onAddMoreFilesClick(DefaultAttachmentTypes.file); - }, - child: Container( - constraints: const BoxConstraints.expand(), - color: widget.streamChatTheme.colorTheme.inputBg, - alignment: Alignment.center, - child: Text( - context.translations.addMoreFilesLabel, - style: TextStyle( - color: widget.streamChatTheme.colorTheme.accentPrimary, - fontWeight: FontWeight.bold, - ), - ), - ), - ); - } - return StreamMediaListView( - selectedIds: widget.selectedMedias, - onSelect: widget.onMediaSelected, - controller: widget.mediaListViewController, - thumbnailSize: widget.mediaThumbnailSize, - thumbnailFormat: widget.mediaThumbnailFormat, - thumbnailQuality: widget.mediaThumbnailQuality, - thumbnailScale: widget.mediaThumbnailScale, - ); - } - - return InkWell( - onTap: () async { - PhotoManager.openSetting(); - }, - child: ColoredBox( - color: widget.streamChatTheme.colorTheme.inputBg, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SvgPicture.asset( - 'svgs/icon_picture_empty_state.svg', - package: 'stream_chat_flutter', - height: 140, - color: widget.streamChatTheme.colorTheme.disabled, - ), - Text( - context.translations.enablePhotoAndVideoAccessMessage, - style: widget.streamChatTheme.textTheme.body.copyWith( - color: widget.streamChatTheme.colorTheme.textLowEmphasis, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 6), - Center( - child: Text( - context.translations.allowGalleryAccessMessage, - style: widget.streamChatTheme.textTheme.bodyBold.copyWith( - color: widget.streamChatTheme.colorTheme.accentPrimary, - ), - ), - ), - ], - ), - ), - ); - }, - ); - } -} - -/// Class which holds data for a custom attachment type in the attachment picker -class CustomAttachmentType { - /// Default constructor for creating a custom attachment for the attachment - /// picker. - CustomAttachmentType({ - required this.type, - required this.iconBuilder, - required this.pickerBuilder, - }); - - /// Type name. - String type; - - /// Builds the icon in the attachment picker top row. - CustomAttachmentIconBuilder iconBuilder; - - /// Builds content in the attachment builder when icon is selected. - WidgetBuilder pickerBuilder; -} diff --git a/packages/stream_chat_flutter/lib/src/video/video_service.dart b/packages/stream_chat_flutter/lib/src/video/video_service.dart new file mode 100644 index 00000000..76214dbc --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/video/video_service.dart @@ -0,0 +1,83 @@ +import 'dart:async'; +import 'dart:ui' as ui; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:stream_chat_flutter/src/utils/device_segmentation.dart'; +import 'package:thumblr/thumblr.dart' as thumblr; +import 'package:video_thumbnail/video_thumbnail.dart'; + +/// +// ignore: prefer-match-file-name +class _IVideoService { + _IVideoService._(); + + /// Singleton instance of [_IVideoService] + static final _IVideoService instance = _IVideoService._(); + + /// Generates a thumbnail image data in memory as UInt8List. + /// + /// The video source can be a local video file or a URL. + /// + /// Thumbnails are not supported on Web at this time. + /// + /// For desktop, you can specify the position of the video to generate + /// the thumbnail. + /// + /// For mobile, you can specify the maximum height or width for the thumbnail + /// or 0 for same resolution as the original video. The lower quality value + /// creates lower quality of the thumbnail image, but it gets ignored for + /// PNG format. + Future generateVideoThumbnail({ + required String video, + ImageFormat imageFormat = ImageFormat.PNG, + int maxHeight = 0, + int maxWidth = 0, + int timeMs = 0, + int quality = 10, + }) async { + if (kIsWeb) { + final placeholder = await generatePlaceholderThumbnail(); + return placeholder; + } + if (isDesktopDevice) { + try { + final thumbnail = await thumblr.generateThumbnail(filePath: video); + final byteData = await thumbnail.image.toByteData( + format: ui.ImageByteFormat.png, + ); + final bytesList = byteData?.buffer.asUint8List() ?? Uint8List(0); + if (bytesList.isNotEmpty) { + return bytesList; + } else { + return await generatePlaceholderThumbnail(); + } + } catch (e) { + // If the thumbnail generation fails, return a placeholder image. + final placeholder = await generatePlaceholderThumbnail(); + return placeholder; + } + } else if (isMobileDevice) { + return VideoThumbnail.thumbnailData( + video: video, + imageFormat: imageFormat, + maxHeight: maxHeight, + maxWidth: maxWidth, + timeMs: timeMs, + quality: quality, + ); + } + throw Exception('Could not generate thumbnail'); + } + + /// Generates a placeholder thumbnail by loading placeholder.png from assets. + Future generatePlaceholderThumbnail() async { + final placeholder = await rootBundle + .load('packages/stream_chat_flutter/images/placeholder.png'); + return placeholder.buffer.asUint8List(); + } +} + +/// Get instance of [_IVideoService] +// ignore: non_constant_identifier_names +_IVideoService get StreamVideoService => _IVideoService.instance; diff --git a/packages/stream_chat_flutter/lib/src/video_thumbnail_image.dart b/packages/stream_chat_flutter/lib/src/video/video_thumbnail_image.dart similarity index 76% rename from packages/stream_chat_flutter/lib/src/video_thumbnail_image.dart rename to packages/stream_chat_flutter/lib/src/video/video_thumbnail_image.dart index de00d14e..f6a6ed56 100644 --- a/packages/stream_chat_flutter/lib/src/video_thumbnail_image.dart +++ b/packages/stream_chat_flutter/lib/src/video/video_thumbnail_image.dart @@ -2,24 +2,19 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; -import 'package:stream_chat_flutter/src/video_service.dart'; +import 'package:stream_chat_flutter/src/video/video_service.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:video_thumbnail/video_thumbnail.dart'; -/// {@macro video_thumbnail_image} -@Deprecated("Use 'StreamVideoThumbnailImage' instead") -typedef VideoThumbnailImage = StreamVideoThumbnailImage; - -/// {@template video_thumbnail_image} -/// Widget for creating video thumbnail image +/// {@template streamVideoThumbnailImage} +/// Displays a video thumbnail for video attachments in a message. /// {@endtemplate} class StreamVideoThumbnailImage extends StatefulWidget { - /// Constructor for creating [StreamVideoThumbnailImage] + /// {@macro streamVideoThumbnailImage} const StreamVideoThumbnailImage({ super.key, required this.video, - this.width, - this.height, + this.constraints, this.fit, this.format = ImageFormat.PNG, this.errorBuilder, @@ -29,22 +24,20 @@ class StreamVideoThumbnailImage extends StatefulWidget { /// Video path final String video; - /// Width of widget - final double? width; + /// Contraints of attachments + final BoxConstraints? constraints; - /// Height of widget - final double? height; - - /// Fit of iamge + /// Fit of image final BoxFit? fit; /// Image format final ImageFormat format; - /// Builds widget on error + /// A builder for building a custom error widget when the thumbnail + /// creation fails final Widget Function(BuildContext, Object?)? errorBuilder; - /// Builds placeholder + /// A builder for building custom thumbnail loading UI final WidgetBuilder? placeholderBuilder; @override @@ -58,17 +51,17 @@ class _StreamVideoThumbnailImageState extends State { @override void initState() { + super.initState(); thumbnailFuture = StreamVideoService.generateVideoThumbnail( video: widget.video, imageFormat: widget.format, ); - super.initState(); } @override void didChangeDependencies() { - _streamChatTheme = StreamChatTheme.of(context); super.didChangeDependencies(); + _streamChatTheme = StreamChatTheme.of(context); } @override @@ -83,7 +76,10 @@ class _StreamVideoThumbnailImageState extends State { } @override - Widget build(BuildContext context) => FutureBuilder( + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: widget.constraints ?? const BoxConstraints.expand(), + child: FutureBuilder( future: thumbnailFuture, builder: (context, snapshot) => AnimatedSwitcher( duration: const Duration(milliseconds: 350), @@ -107,8 +103,8 @@ class _StreamVideoThumbnailImageState extends State { child: Image.asset( 'images/placeholder.png', fit: BoxFit.cover, - height: widget.height, - width: widget.width, + height: widget.constraints?.maxHeight, + width: widget.constraints?.maxWidth, package: 'stream_chat_flutter', ), ), @@ -120,12 +116,14 @@ class _StreamVideoThumbnailImageState extends State { child: Image.memory( snapshot.data!, fit: widget.fit, - height: widget.height, - width: widget.width, + height: widget.constraints?.maxHeight ?? double.infinity, + width: widget.constraints?.maxWidth ?? double.infinity, ), ); }, ), ), - ); + ), + ); + } } diff --git a/packages/stream_chat_flutter/lib/src/video/vlc/vlc_manager.dart b/packages/stream_chat_flutter/lib/src/video/vlc/vlc_manager.dart new file mode 100644 index 00000000..f721e244 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/video/vlc/vlc_manager.dart @@ -0,0 +1,22 @@ +import 'package:stream_chat_flutter/src/video/vlc/vlc_stub.dart' + if (dart.library.io) 'vlc_manager_desktop.dart' + if (dar.library.html) 'vlc_manager_web.dart'; + +/// {@template vlcManager} +/// An abstract class for the purpose of ensuring Flutter applications that +/// target both desktop & web do not crash when building for web targets. +/// {@endtemplate} +abstract class VlcManager { + // ignore: use_late_for_private_fields_and_variables + static VlcManager? _instance; + + /// The current instance of [VlcManager]. + static VlcManager get instance { + _instance = getVlc(); + + return _instance!; + } + + /// Initializes VLC. + void initialize(); +} diff --git a/packages/stream_chat_flutter/lib/src/video/vlc/vlc_manager_desktop.dart b/packages/stream_chat_flutter/lib/src/video/vlc/vlc_manager_desktop.dart new file mode 100644 index 00000000..3f86a6e6 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/video/vlc/vlc_manager_desktop.dart @@ -0,0 +1,13 @@ +import 'package:dart_vlc/dart_vlc.dart'; +import 'package:stream_chat_flutter/src/video/vlc/vlc_manager.dart'; + +/// The desktop implementation of [VlcManager]. It simply initializes VLC. +class VlcManagerDesktop extends VlcManager { + @override + void initialize() { + DartVLC.initialize(); + } +} + +/// Allows [VlcManager] to return the correct implementation. +VlcManager getVlc() => VlcManagerDesktop(); diff --git a/packages/stream_chat_flutter/lib/src/video/vlc/vlc_manager_web.dart b/packages/stream_chat_flutter/lib/src/video/vlc/vlc_manager_web.dart new file mode 100644 index 00000000..3462350b --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/video/vlc/vlc_manager_web.dart @@ -0,0 +1,14 @@ +import 'package:flutter/rendering.dart'; +import 'package:stream_chat_flutter/src/video/vlc/vlc_manager.dart'; + +/// The web implementation of [VlcManager]. Naturally, it does nothing. It +/// exists simply to satisfy the requirements of conditional imports. +class VlcManagerWeb extends VlcManager { + @override + void initialize() { + debugPrint('Stub initialization for VLC.'); + } +} + +/// Allows [VlcManager] to return the correct implementation. +VlcManager getVlc() => VlcManagerWeb(); diff --git a/packages/stream_chat_flutter/lib/src/video/vlc/vlc_stub.dart b/packages/stream_chat_flutter/lib/src/video/vlc/vlc_stub.dart new file mode 100644 index 00000000..289961fd --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/video/vlc/vlc_stub.dart @@ -0,0 +1,5 @@ +import 'package:stream_chat_flutter/src/video/vlc/vlc_manager.dart'; + +/// Method stub for getting the right instance of [VlcManager] on the right +/// platform. +VlcManager getVlc() => throw UnsupportedError('Cannot create VLC Manager'); diff --git a/packages/stream_chat_flutter/lib/src/video_service.dart b/packages/stream_chat_flutter/lib/src/video_service.dart deleted file mode 100644 index 120460be..00000000 --- a/packages/stream_chat_flutter/lib/src/video_service.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:video_thumbnail/video_thumbnail.dart'; - -/// -// ignore: prefer-match-file-name -class _IVideoService { - _IVideoService._(); - - /// Singleton instance of [_IVideoService] - static final _IVideoService instance = _IVideoService._(); - - /// Generates a thumbnail image data in memory as UInt8List, - /// it can be easily used by Image.memory(...). - /// The video can be a local video file, or an URL repreents iOS or - /// Android native supported video format. - /// Speicify the maximum height or width for the thumbnail or 0 for - /// same resolution as the original video. - /// The lower quality value creates lower quality of the thumbnail image, - /// but it gets ignored for PNG format. - Future generateVideoThumbnail({ - required String video, - ImageFormat imageFormat = ImageFormat.PNG, - int maxHeight = 0, - int maxWidth = 0, - int timeMs = 0, - int quality = 10, - }) => - VideoThumbnail.thumbnailData( - video: video, - imageFormat: imageFormat, - maxHeight: maxHeight, - maxWidth: maxWidth, - timeMs: timeMs, - quality: quality, - ); -} - -/// Get instance of [_IVideoService] -@Deprecated("Use 'StreamVideoService' instead") -// ignore: non_constant_identifier_names -_IVideoService get VideoService => _IVideoService.instance; - -/// Get instance of [_IVideoService] -// ignore: non_constant_identifier_names -_IVideoService get StreamVideoService => _IVideoService.instance; diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 091f8ae4..b5e44c56 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -1,79 +1,100 @@ export 'package:jiffy/jiffy.dart'; export 'package:photo_manager/photo_manager.dart' show ThumbnailSize, ThumbnailFormat; +export 'package:stream_chat_flutter/src/message_widget/parse_attachments.dart'; +export 'package:stream_chat_flutter/src/message_widget/quoted_message.dart'; export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; export 'src/attachment/attachment.dart'; -export 'src/attachment_actions_modal.dart'; -export 'src/back_button.dart'; -export 'src/channel_avatar.dart'; -export 'src/channel_header.dart'; -export 'src/channel_list_header.dart'; -export 'src/channel_list_view.dart'; -export 'src/channel_name.dart'; -export 'src/channel_preview.dart'; -export 'src/connection_status_builder.dart'; -export 'src/date_divider.dart'; -export 'src/deleted_message.dart'; -export 'src/extension.dart' show IconButtonX; -export 'src/full_screen_media.dart'; -export 'src/gallery_footer.dart'; -export 'src/gallery_header.dart'; -export 'src/gradient_avatar.dart'; -export 'src/info_tile.dart'; +export 'src/attachment/attachment_title.dart'; +export 'src/attachment/handler/stream_attachment_handler.dart'; +export 'src/attachment/image_attachment.dart'; +export 'src/attachment/image_group.dart'; +export 'src/attachment/stream_attachment_package.dart'; +export 'src/attachment/url_attachment.dart'; +export 'src/attachment/video_attachment.dart'; +export 'src/attachment_actions_modal/attachment_actions_modal.dart'; +export 'src/autocomplete/stream_autocomplete.dart'; +export 'src/avatars/gradient_avatar.dart'; +export 'src/avatars/group_avatar.dart'; +export 'src/avatars/user_avatar.dart'; +export 'src/bottom_sheets/attachment_modal_sheet.dart'; +export 'src/bottom_sheets/edit_message_sheet.dart'; +export 'src/bottom_sheets/error_alert_sheet.dart'; +export 'src/bottom_sheets/stream_channel_info_bottom_sheet.dart'; +export 'src/channel/channel_header.dart'; +export 'src/channel/channel_info.dart'; +export 'src/channel/channel_list_header.dart'; +export 'src/channel/channel_name.dart'; +export 'src/channel/channel_preview.dart'; +export 'src/channel/stream_channel_avatar.dart'; +export 'src/channel/stream_channel_name.dart'; +export 'src/channel/stream_message_preview_text.dart'; +export 'src/fullscreen_media/fsm_enums.dart'; +export 'src/fullscreen_media/full_screen_media.dart'; +export 'src/fullscreen_media/full_screen_media_builder.dart'; +export 'src/gallery/gallery_footer.dart'; +export 'src/gallery/gallery_header.dart'; +export 'src/indicators/sending_indicator.dart'; +export 'src/indicators/typing_indicator.dart'; +export 'src/indicators/unread_indicator.dart'; +export 'src/indicators/upload_progress_indicator.dart'; +export 'src/keyboard_shortcuts/keyboard_shortcut_runner.dart'; export 'src/localization/stream_chat_localizations.dart'; export 'src/localization/translations.dart' show DefaultTranslations; -export 'src/message_action.dart'; - -// ignore: deprecated_member_use_from_same_package -export 'src/message_input.dart' show MessageInput, MessageInputState; -export 'src/message_list_view.dart'; -export 'src/message_search_item.dart'; -export 'src/message_search_list_view.dart'; -export 'src/message_text.dart'; -export 'src/message_widget.dart'; -export 'src/multi_overlay.dart'; -export 'src/option_list_tile.dart'; -export 'src/reaction_icon.dart'; -export 'src/reaction_picker.dart'; -export 'src/sending_indicator.dart'; -export 'src/stream_attachment_package.dart'; +export 'src/message_actions_modal/message_action.dart'; +export 'src/message_input/attachment_picker/stream_attachment_picker.dart'; +export 'src/message_input/attachment_picker/stream_attachment_picker_bottom_sheet.dart'; +export 'src/message_input/countdown_button.dart'; +export 'src/message_input/enums.dart'; +export 'src/message_input/stream_message_input.dart'; +export 'src/message_input/stream_message_send_button.dart'; +export 'src/message_input/stream_message_text_field.dart'; +export 'src/message_list_view/message_details.dart'; +export 'src/message_list_view/message_list_view.dart'; +export 'src/message_widget/deleted_message.dart'; +export 'src/message_widget/message_text.dart'; +export 'src/message_widget/message_widget.dart'; +export 'src/message_widget/reactions/reaction_picker.dart'; +export 'src/message_widget/text_bubble.dart'; +export 'src/misc/back_button.dart'; +export 'src/misc/connection_status_builder.dart'; +export 'src/misc/date_divider.dart'; +export 'src/misc/info_tile.dart'; +export 'src/misc/option_list_tile.dart'; +export 'src/misc/reaction_icon.dart'; +export 'src/misc/stream_neumorphic_button.dart'; +export 'src/misc/stream_svg_icon.dart'; +export 'src/misc/system_message.dart'; +export 'src/misc/thread_header.dart'; +export 'src/misc/visible_footnote.dart'; +export 'src/scroll_view/channel_scroll_view/stream_channel_grid_tile.dart'; +export 'src/scroll_view/channel_scroll_view/stream_channel_grid_view.dart'; +export 'src/scroll_view/channel_scroll_view/stream_channel_list_tile.dart'; +export 'src/scroll_view/channel_scroll_view/stream_channel_list_view.dart'; +export 'src/scroll_view/member_scroll_view/stream_member_grid_view.dart'; +export 'src/scroll_view/member_scroll_view/stream_member_list_view.dart'; +export 'src/scroll_view/message_search_scroll_view/stream_message_search_grid_view.dart'; +export 'src/scroll_view/message_search_scroll_view/stream_message_search_list_tile.dart'; +export 'src/scroll_view/message_search_scroll_view/stream_message_search_list_view.dart'; +export 'src/scroll_view/photo_gallery/stream_photo_gallery.dart'; +export 'src/scroll_view/photo_gallery/stream_photo_gallery_controller.dart'; +export 'src/scroll_view/photo_gallery/stream_photo_gallery_tile.dart'; +export 'src/scroll_view/stream_scroll_view_empty_widget.dart'; +export 'src/scroll_view/stream_scroll_view_indexed_widget_builder.dart'; +export 'src/scroll_view/user_scroll_view/stream_user_grid_tile.dart'; +export 'src/scroll_view/user_scroll_view/stream_user_grid_tile.dart'; +export 'src/scroll_view/user_scroll_view/stream_user_grid_view.dart'; +export 'src/scroll_view/user_scroll_view/stream_user_grid_view.dart'; +export 'src/scroll_view/user_scroll_view/stream_user_list_tile.dart'; +export 'src/scroll_view/user_scroll_view/stream_user_list_view.dart'; export 'src/stream_chat.dart'; -export 'src/stream_chat_theme.dart'; -export 'src/stream_neumorphic_button.dart'; -export 'src/stream_svg_icon.dart'; -export 'src/system_message.dart'; +export 'src/stream_chat_configuration.dart'; +export 'src/theme/stream_chat_theme.dart'; export 'src/theme/themes.dart'; -export 'src/thread_header.dart'; -export 'src/typing_indicator.dart'; -export 'src/unread_indicator.dart'; -export 'src/user_avatar.dart'; -export 'src/user_item.dart'; -export 'src/user_list_view.dart'; -export 'src/user_mention_tile.dart'; -export 'src/utils.dart'; - -// v4 -export 'src/v4/message_input/countdown_button.dart'; -export 'src/v4/message_input/stream_attachment_picker.dart'; -export 'src/v4/message_input/stream_message_input.dart'; -export 'src/v4/message_input/stream_message_send_button.dart'; -export 'src/v4/message_input/stream_message_text_field.dart'; -export 'src/v4/scroll_view/channel_scroll_view/stream_channel_grid_tile.dart'; -export 'src/v4/scroll_view/channel_scroll_view/stream_channel_grid_view.dart'; -export 'src/v4/scroll_view/channel_scroll_view/stream_channel_list_tile.dart'; -export 'src/v4/scroll_view/channel_scroll_view/stream_channel_list_view.dart'; -export 'src/v4/scroll_view/message_search_scroll_view/stream_message_search_grid_view.dart'; -export 'src/v4/scroll_view/message_search_scroll_view/stream_message_search_list_tile.dart'; -export 'src/v4/scroll_view/message_search_scroll_view/stream_message_search_list_view.dart'; -export 'src/v4/scroll_view/stream_scroll_view_empty_widget.dart'; -export 'src/v4/scroll_view/stream_scroll_view_indexed_widget_builder.dart'; -export 'src/v4/scroll_view/user_scroll_view/stream_user_grid_tile.dart'; -export 'src/v4/scroll_view/user_scroll_view/stream_user_grid_view.dart'; -export 'src/v4/scroll_view/user_scroll_view/stream_user_list_tile.dart'; -export 'src/v4/scroll_view/user_scroll_view/stream_user_list_view.dart'; -export 'src/v4/stream_channel_avatar.dart'; -export 'src/v4/stream_channel_info_bottom_sheet.dart'; -export 'src/v4/stream_channel_name.dart'; -export 'src/v4/stream_message_preview_text.dart'; -export 'src/visible_footnote.dart'; +export 'src/user/user_item.dart'; +export 'src/user/user_mention_tile.dart'; +export 'src/utils/device_segmentation.dart'; +export 'src/utils/extensions.dart' show IconButtonX; +export 'src/utils/helpers.dart'; +export 'src/utils/typedefs.dart'; diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index c0872593..7704c1f7 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter. -version: 4.6.0 +version: 5.0.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -11,35 +11,40 @@ environment: dependencies: cached_network_image: ^3.0.0 - characters: ^1.1.0 - chewie: ^1.3.0 + chewie: ^1.3.4 collection: ^1.15.0 + contextmenu: ^3.0.0 + dart_vlc: ^0.3.0 + desktop_drop: ^0.3.3 diacritic: ^0.1.3 dio: ^4.0.6 ezanimation: ^0.6.0 - file_picker: ^5.0.0 + file_picker: ^4.1.3 + file_selector: ^0.9.0 flutter: sdk: flutter flutter_markdown: ^0.6.1 flutter_portal: ^1.0.0 - flutter_slidable: ^2.0.0 flutter_svg: ^1.0.1 + http: ^0.13.4 http_parser: ^4.0.0 image_gallery_saver: ^1.7.0 image_picker: ^0.8.2 jiffy: ^5.0.0 lottie: ^1.0.1 meta: ^1.3.0 - path_provider: ^2.0.1 + path_provider: ^2.0.9 photo_manager: ^2.0.1 photo_view: ^0.14.0 rxdart: ^0.27.0 share_plus: ^4.0.1 shimmer: ^2.0.0 - stream_chat_flutter_core: ^4.6.0 - substring_highlight: ^1.0.26 + stream_chat_flutter_core: ^5.0.0 + synchronized: ^3.0.0 + thumblr: ^0.0.4 url_launcher: ^6.1.0 - video_player: ^2.1.0 + video_player: ^2.4.5 + video_player_macos: ^1.0.6 video_thumbnail: ^0.5.0 flutter: diff --git a/packages/stream_chat_flutter/test/conditional_parent_builder/conditional_parent_builder_test.dart b/packages/stream_chat_flutter/test/conditional_parent_builder/conditional_parent_builder_test.dart new file mode 100644 index 00000000..c52c28ab --- /dev/null +++ b/packages/stream_chat_flutter/test/conditional_parent_builder/conditional_parent_builder_test.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/conditional_parent_builder/conditional_parent_builder.dart'; + +void main() { + testWidgets('ConditionalParentBuilder builds the parent widget', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: ConditionalParentBuilder( + builder: (context, child) => Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + child, + ], + ), + child: const Text('Hello World!'), + ), + ), + ), + ), + ); + expect(find.byType(Column), findsOneWidget); + expect(find.byType(Text), findsOneWidget); + }); + + testWidgets('ConditionalParentBuilder does not build the parent widget', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: ConditionalParentBuilder( + builder: (context, child) { + return child; + }, + child: const Text('Hello World!'), + ), + ), + ), + ), + ); + expect(find.byType(Column), findsNothing); + expect(find.byType(Text), findsOneWidget); + }); +} diff --git a/packages/stream_chat_flutter/test/flutter_test_config.dart b/packages/stream_chat_flutter/test/flutter_test_config.dart index 07b2da98..d5332f3a 100644 --- a/packages/stream_chat_flutter/test/flutter_test_config.dart +++ b/packages/stream_chat_flutter/test/flutter_test_config.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/packages/stream_chat_flutter/test/platform_widget_builder/desktop_widget_builder_test.dart b/packages/stream_chat_flutter/test/platform_widget_builder/desktop_widget_builder_test.dart new file mode 100644 index 00000000..a7b867ce --- /dev/null +++ b/packages/stream_chat_flutter/test/platform_widget_builder/desktop_widget_builder_test.dart @@ -0,0 +1,77 @@ +import 'package:flutter/foundation.dart' + show debugDefaultTargetPlatformOverride; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/platform_widget_builder/platform_widget_builder.dart'; + +void main() { + testWidgets( + 'PlatformWidgetBuilder builds the correct widget for mobile', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: DesktopWidgetBuilder( + macOS: (context, child) => Text( + '$debugDefaultTargetPlatformOverride', + ), + ), + ), + ), + ), + ); + + expect(find.text('$debugDefaultTargetPlatformOverride'), findsOneWidget); + }, + variant: const TargetPlatformVariant({ + TargetPlatform.macOS, + }), + ); + + testWidgets( + 'PlatformWidgetBuilder builds the correct widget for desktop', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: DesktopWidgetBuilder( + windows: (context, child) => Text( + '$debugDefaultTargetPlatformOverride', + ), + ), + ), + ), + ), + ); + + expect(find.text('$debugDefaultTargetPlatformOverride'), findsOneWidget); + }, + variant: const TargetPlatformVariant({ + TargetPlatform.windows, + }), + ); + + testWidgets( + 'PlatformWidgetBuilder builds the correct widget for web', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: DesktopWidgetBuilder( + linux: (context, child) => const Text('Web'), + ), + ), + ), + ), + ); + + expect(find.text('Web'), findsOneWidget); + }, + variant: const TargetPlatformVariant({ + TargetPlatform.linux, + }), + ); +} diff --git a/packages/stream_chat_flutter/test/platform_widget_builder/platform_widget_builder_test.dart b/packages/stream_chat_flutter/test/platform_widget_builder/platform_widget_builder_test.dart new file mode 100644 index 00000000..08425ebc --- /dev/null +++ b/packages/stream_chat_flutter/test/platform_widget_builder/platform_widget_builder_test.dart @@ -0,0 +1,74 @@ +import 'package:flutter/foundation.dart' + show debugDefaultTargetPlatformOverride; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/platform_widget_builder/platform_widget_builder.dart'; + +void main() { + testWidgets( + 'PlatformWidgetBuilder builds the correct widget for mobile', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: PlatformWidgetBuilder( + mobile: (context, child) => Text( + '$debugDefaultTargetPlatformOverride', + ), + ), + ), + ), + ), + ); + + expect(find.text('$debugDefaultTargetPlatformOverride'), findsOneWidget); + }, + variant: const TargetPlatformVariant({ + TargetPlatform.android, + TargetPlatform.iOS, + }), + ); + + testWidgets( + 'PlatformWidgetBuilder builds the correct widget for desktop', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: PlatformWidgetBuilder( + desktop: (context, child) => Text( + '$debugDefaultTargetPlatformOverride', + ), + ), + ), + ), + ), + ); + + expect(find.text('$debugDefaultTargetPlatformOverride'), findsOneWidget); + }, + variant: TargetPlatformVariant.desktop(), + ); + + testWidgets( + 'PlatformWidgetBuilder builds the correct widget for web', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: PlatformWidgetBuilder( + web: (context, child) => const Text('Web'), + ), + ), + ), + ), + ); + + expect(find.text('Web'), findsOneWidget); + }, + variant: const TargetPlatformVariant({TargetPlatform.fuchsia}), // hacky :/ + ); +} diff --git a/packages/stream_chat_flutter/test/src/attachment/attachment_error_test.dart b/packages/stream_chat_flutter/test/src/attachment/attachment_error_test.dart new file mode 100644 index 00000000..6e038505 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/attachment/attachment_error_test.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../mocks.dart'; + +void main() { + testWidgets('AttachmentError test', (tester) async { + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChat( + client: MockClient(), + child: child, + ), + home: const Scaffold( + body: Center( + child: AttachmentError(), + ), + ), + ), + ); + + expect(find.byType(Icon), findsOneWidget); + }); +} diff --git a/packages/stream_chat_flutter/test/src/attachment/attachment_handler_test.dart b/packages/stream_chat_flutter/test/src/attachment/attachment_handler_test.dart new file mode 100644 index 00000000..67654344 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/attachment/attachment_handler_test.dart @@ -0,0 +1,68 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../mocks.dart'; + +void main() { + group('AttachmentHandler Downloads', () { + test('AttachmentHandler downloads image', () async { + final attachment = Attachment( + title: 'test image attachment', + type: 'image', + extraData: const { + 'mime_type': 'png', + }, + ); + + final attachmentHandler = MockAttachmentHandler(); + + when(() => attachmentHandler.downloadAttachment(attachment)) + .thenAnswer((invocation) async => 'filePath'); + + expect( + await attachmentHandler.downloadAttachment(attachment), + 'filePath', + ); + }); + + test('AttachmentHandler downloads giphy', () async { + final attachment = Attachment( + title: 'test giphy attachment', + type: 'giphy', + extraData: const { + 'original': + 'https://giphy.com/gifs/nrkp3-dance-happy-3o7TKnCdBx5cMg0qti', + }, + ); + + final attachmentHandler = MockAttachmentHandler(); + + when(() => attachmentHandler.downloadAttachment(attachment)) + .thenAnswer((invocation) async => 'filePath'); + + expect( + await attachmentHandler.downloadAttachment(attachment), + 'filePath', + ); + }); + + test('AttachmentHandler downloads video', () async { + final attachment = Attachment( + title: 'test video attachment', + type: 'video', + assetUrl: 'https://www.youtube.com/watch?v=lytQi-slT5Y', + ); + + final attachmentHandler = MockAttachmentHandler(); + + when(() => attachmentHandler.downloadAttachment(attachment)) + .thenAnswer((invocation) async => 'filePath'); + + expect( + await attachmentHandler.downloadAttachment(attachment), + 'filePath', + ); + }); + }); +} diff --git a/packages/stream_chat_flutter/test/src/attachment/attachment_title_test.dart b/packages/stream_chat_flutter/test/src/attachment/attachment_title_test.dart new file mode 100644 index 00000000..bfe1223e --- /dev/null +++ b/packages/stream_chat_flutter/test/src/attachment/attachment_title_test.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../mocks.dart'; + +void main() { + testWidgets('AttachmentTitle renders properly', (tester) async { + final mockClient = MockClient(); + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChat( + client: mockClient, + streamChatThemeData: StreamChatThemeData.light(), + child: child, + ), + home: Scaffold( + body: Builder( + builder: (context) { + return Center( + child: StreamAttachmentTitle( + attachment: Attachment( + title: 'Test Attachment', + type: 'video', + titleLink: 'https://www.youtube.com/watch?v=lytQi-slT5Y', + ogScrapeUrl: 'https://www.youtube.com/watch?v=lytQi-slT5Y', + ), + messageTheme: StreamChatTheme.of(context).ownMessageTheme, + ), + ); + }, + ), + ), + ), + ); + + expect(find.byType(StreamAttachmentTitle), findsOneWidget); + expect(find.text('Test Attachment'), findsOneWidget); + expect(find.text('https://www.youtube.com/watch?v=lytQi-slT5Y'), + findsOneWidget); + }); +} diff --git a/packages/stream_chat_flutter/test/src/attachment/attachment_upload_state_builder_test.dart b/packages/stream_chat_flutter/test/src/attachment/attachment_upload_state_builder_test.dart new file mode 100644 index 00000000..f87bcded --- /dev/null +++ b/packages/stream_chat_flutter/test/src/attachment/attachment_upload_state_builder_test.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../mocks.dart'; + +void main() { + testWidgets( + 'AttachmentUploadStateBuilder returns Offstage when message is sent', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: StreamChannel( + channel: MockChannel(), + child: StreamAttachmentUploadStateBuilder( + attachment: Attachment( + id: 'test', + ), + message: Message( + id: 'test', + ), + ), + ), + ), + ), + ), + ); + + expect(find.byType(Offstage), findsOneWidget); + }); +} diff --git a/packages/stream_chat_flutter/test/src/attachment_widgets_test.dart b/packages/stream_chat_flutter/test/src/attachment/file_attachment_test.dart similarity index 90% rename from packages/stream_chat_flutter/test/src/attachment_widgets_test.dart rename to packages/stream_chat_flutter/test/src/attachment/file_attachment_test.dart index 9747f11d..080cf6f5 100644 --- a/packages/stream_chat_flutter/test/src/attachment_widgets_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment/file_attachment_test.dart @@ -3,11 +3,11 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { testWidgets( - 'it should show file details', + 'Shows the file name', (WidgetTester tester) async { final channel = MockChannel(); final channelState = MockChannelState(); @@ -25,10 +25,10 @@ void main() { channel: channel, child: SizedBox( child: StreamFileAttachment( - size: const Size( + constraints: BoxConstraints.tight(const Size( 300, 300, - ), + )), message: Message(), attachment: Attachment( type: 'file', diff --git a/packages/stream_chat_flutter/test/src/attachment/giphy_attachment_test.dart b/packages/stream_chat_flutter/test/src/attachment/giphy_attachment_test.dart new file mode 100644 index 00000000..0399f1e8 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/attachment/giphy_attachment_test.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../mocks.dart'; + +void main() { + testWidgets( + 'Shows GIPHY text', + (WidgetTester tester) async { + final channel = MockChannel(); + final channelState = MockChannelState(); + + when(() => channel.state).thenReturn(channelState); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.fromTheme(themeData); + + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: streamTheme, + child: StreamChannel( + channel: channel, + child: SizedBox( + child: StreamGiphyAttachment( + constraints: BoxConstraints.tight(const Size( + 300, + 300, + )), + message: Message(), + attachment: Attachment( + type: 'giphy', + title: 'example.gif', + imageUrl: + 'https://media.giphy.com/media/35H0pwQNaO2iLTnnBf/giphy.gif', + extraData: const { + 'mime_type': 'gif', + }, + ), + ), + ), + ), + ), + ), + ); + + expect(find.text('GIPHY'), findsOneWidget); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/attachment/image_attachment_test.dart b/packages/stream_chat_flutter/test/src/attachment/image_attachment_test.dart new file mode 100644 index 00000000..b90f28a4 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/attachment/image_attachment_test.dart @@ -0,0 +1,54 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../mocks.dart'; + +void main() { + testWidgets( + 'Shows the image', + (WidgetTester tester) async { + final channel = MockChannel(); + final channelState = MockChannelState(); + + when(() => channel.state).thenReturn(channelState); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.fromTheme(themeData); + + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: streamTheme, + child: StreamChannel( + channel: channel, + child: SizedBox( + child: StreamImageAttachment( + messageTheme: streamTheme.ownMessageTheme, + constraints: BoxConstraints.tight(const Size( + 300, + 300, + )), + message: Message(), + attachment: Attachment( + type: 'image', + title: 'example.png', + imageUrl: + 'https://logowik.com/content/uploads/images/flutter5786.jpg', + extraData: const { + 'mime_type': 'png', + }, + ), + ), + ), + ), + ), + ), + ); + + expect(find.byType(CachedNetworkImage), findsOneWidget); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/attachment/image_group_test.dart b/packages/stream_chat_flutter/test/src/attachment/image_group_test.dart new file mode 100644 index 00000000..43fea9e7 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/attachment/image_group_test.dart @@ -0,0 +1,65 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../mocks.dart'; + +void main() { + testWidgets( + 'Shows 2 images', + (WidgetTester tester) async { + final channel = MockChannel(); + final channelState = MockChannelState(); + + when(() => channel.state).thenReturn(channelState); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.fromTheme(themeData); + + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: streamTheme, + child: StreamChannel( + channel: channel, + child: SizedBox( + child: StreamImageGroup( + messageTheme: streamTheme.ownMessageTheme, + constraints: BoxConstraints.tight(const Size( + 300, + 300, + )), + message: Message(), + images: [ + Attachment( + type: 'image', + title: 'example.png', + imageUrl: + 'https://logowik.com/content/uploads/images/flutter5786.jpg', + extraData: const { + 'mime_type': 'png', + }, + ), + Attachment( + type: 'image', + title: 'example.png', + imageUrl: + 'https://logowik.com/content/uploads/images/flutter5786.jpg', + extraData: const { + 'mime_type': 'png', + }, + ), + ], + ), + ), + ), + ), + ), + ); + + expect(find.byType(CachedNetworkImage), findsNWidgets(2)); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/attachment/url_attachment_test.dart b/packages/stream_chat_flutter/test/src/attachment/url_attachment_test.dart new file mode 100644 index 00000000..b1636fd4 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/attachment/url_attachment_test.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../mocks.dart'; + +void main() { + testWidgets( + 'Shows the attachment title', + (WidgetTester tester) async { + final channel = MockChannel(); + final channelState = MockChannelState(); + + when(() => channel.state).thenReturn(channelState); + + final themeData = ThemeData(); + final streamTheme = StreamChatThemeData.fromTheme(themeData); + + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: streamTheme, + child: StreamChannel( + channel: channel, + child: SizedBox( + child: StreamUrlAttachment( + messageTheme: streamTheme.ownMessageTheme, + hostDisplayName: 'Test', + urlAttachment: Attachment( + title: 'Flutter', + titleLink: 'https://flutter.dev', + ), + ), + ), + ), + ), + ), + ); + + expect(find.text('Flutter'), findsOneWidget); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart b/packages/stream_chat_flutter/test/src/attachment_actions_modal/attachment_actions_modal_test.dart similarity index 74% rename from packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart rename to packages/stream_chat_flutter/test/src/attachment_actions_modal/attachment_actions_modal_test.dart index 244eace5..2c19fe25 100644 --- a/packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment_actions_modal/attachment_actions_modal_test.dart @@ -5,20 +5,21 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; class MockAttachmentDownloader extends Mock { - ProgressCallback? progressCallback; - DownloadedPathCallback? downloadedPathCallback; + ProgressCallback? onReceiveProgress; Completer completer = Completer(); Future call( Attachment attachment, { - ProgressCallback? progressCallback, - DownloadedPathCallback? downloadedPathCallback, + ProgressCallback? onReceiveProgress, + Map? queryParameters, + CancelToken? cancelToken, + bool deleteOnError = true, + Options? options, }) { - this.progressCallback = progressCallback; - this.downloadedPathCallback = downloadedPathCallback; + this.onReceiveProgress = onReceiveProgress; return completer.future; } } @@ -408,123 +409,4 @@ void main() { verify(() => mockChannel.deleteMessage(message)).called(1); }, ); - - testWidgets( - 'tapping on save in chat should call image downloader', - (WidgetTester tester) async { - final client = MockClient(); - final clientState = MockClientState(); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - - final imageDownloader = MockAttachmentDownloader(); - - final attachment = Attachment( - type: 'image', - title: 'image.jpg', - ); - final message = Message( - text: 'test', - user: User( - id: 'user-id', - ), - attachments: [ - attachment, - ], - ); - - await tester.pumpWidget( - MaterialApp( - builder: (context, child) => StreamChat( - client: client, - child: child, - ), - home: SizedBox( - child: AttachmentActionsModal( - imageDownloader: imageDownloader, - message: message, - attachment: attachment, - ), - ), - ), - ); - - await tester.tap(find.text('Save Image')); - - imageDownloader.progressCallback!(0, 100000); - await tester.pump(); - expect(find.text('0.00 MB'), findsOneWidget); - - imageDownloader.progressCallback!(50000, 100000); - await tester.pump(); - expect(find.text('0.05 MB'), findsOneWidget); - - imageDownloader.progressCallback!(100000, 100000); - imageDownloader.downloadedPathCallback!('path'); - imageDownloader.completer.complete('path'); - await tester.pump(); - expect(find.byKey(const Key('completedIcon')), findsOneWidget); - await tester.pumpAndSettle(const Duration(milliseconds: 500)); - }, - ); - - testWidgets( - 'tapping on save in chat should call file downloader', - (WidgetTester tester) async { - final client = MockClient(); - final clientState = MockClientState(); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - - final fileDownloader = MockAttachmentDownloader(); - - final attachment = Attachment( - type: 'video', - title: 'video.mp4', - ); - final message = Message( - text: 'test', - user: User( - id: 'user-id', - ), - attachments: [ - attachment, - ]); - - await tester.pumpWidget( - MaterialApp( - builder: (context, child) => StreamChat( - client: client, - child: child, - ), - home: SizedBox( - child: AttachmentActionsModal( - fileDownloader: fileDownloader, - message: message, - attachment: attachment, - ), - ), - ), - ); - - await tester.tap(find.text('Save Video')); - - fileDownloader.progressCallback!(0, 100000); - await tester.pump(); - expect(find.text('0.00 MB'), findsOneWidget); - - fileDownloader.progressCallback!(50000, 100000); - await tester.pump(); - expect(find.text('0.05 MB'), findsOneWidget); - - fileDownloader.progressCallback!(100000, 100000); - fileDownloader.downloadedPathCallback!('path'); - fileDownloader.completer.complete('path'); - await tester.pump(); - expect(find.byKey(const Key('completedIcon')), findsOneWidget); - await tester.pumpAndSettle(const Duration(milliseconds: 500)); - }, - ); } diff --git a/packages/stream_chat_flutter/test/src/gradient_avatar_test.dart b/packages/stream_chat_flutter/test/src/avatars/gradient_avatar_test.dart similarity index 99% rename from packages/stream_chat_flutter/test/src/gradient_avatar_test.dart rename to packages/stream_chat_flutter/test/src/avatars/gradient_avatar_test.dart index a4388a2e..04ae7e41 100644 --- a/packages/stream_chat_flutter/test/src/gradient_avatar_test.dart +++ b/packages/stream_chat_flutter/test/src/avatars/gradient_avatar_test.dart @@ -4,7 +4,7 @@ import 'package:golden_toolkit/golden_toolkit.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { testWidgets( diff --git a/packages/stream_chat_flutter/test/src/avatars/group_avatar_test.dart b/packages/stream_chat_flutter/test/src/avatars/group_avatar_test.dart new file mode 100644 index 00000000..5ae770ce --- /dev/null +++ b/packages/stream_chat_flutter/test/src/avatars/group_avatar_test.dart @@ -0,0 +1,227 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../mocks.dart'; + +void main() { + late MockClient client; + late MockChannel channel; + late MockChannelState channelState; + late MockMember member; + late MockUser user; + late MockMember member2; + late MockUser user2; + const methodChannel = + MethodChannel('dev.fluttercommunity.plus/connectivity_status'); + + setUpAll(() { + client = MockClient(); + channel = MockChannel(); + channelState = MockChannelState(); + member = MockMember(); + user = MockUser(); + member2 = MockMember(); + user2 = MockUser(); + + when(() => channel.state!).thenReturn(channelState); + when(() => channelState.membersStream) + .thenAnswer((_) => Stream>.value([member, member2])); + when(() => member.user).thenReturn(user); + when(() => user.name).thenReturn('user123'); + when(() => user.id).thenReturn('123'); + when(() => member2.user).thenReturn(user2); + when(() => user2.name).thenReturn('user456'); + when(() => user2.id).thenReturn('456'); + }); + + setUp(() { + methodChannel.setMockMethodCallHandler((MethodCall methodCall) async { + if (methodCall.method == 'listen') { + try { + await ServicesBinding.instance.defaultBinaryMessenger + .handlePlatformMessage( + methodChannel.name, + methodChannel.codec.encodeSuccessEnvelope('wifi'), + (_) {}, + ); + } catch (e) { + print(e); + } + } + }); + }); + + testWidgets( + 'control test', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + streamChatThemeData: StreamChatThemeData.light(), + child: StreamChannel( + channel: channel, + child: Scaffold( + body: Center( + child: StreamGroupAvatar( + members: [ + member, + member2, + ], + ), + ), + ), + ), + ), + ), + ); + + expect(find.byType(StreamUserAvatar), findsNWidgets(2)); + }, + ); + + testGoldens( + 'golden test for the group with "user123" and "user456"', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + streamChatThemeData: StreamChatThemeData.light(), + child: StreamChannel( + channel: channel, + child: Scaffold( + body: Center( + child: SizedBox( + width: 100, + height: 100, + child: StreamGroupAvatar( + members: [ + member, + member2, + ], + ), + ), + ), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'group_avatar_0'); + }, + ); + + tearDown(() { + methodChannel.setMockMethodCallHandler(null); + }); + + /*testGoldens( + 'golden test for the name "demo user"', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: 100, + height: 100, + child: GroupAvatar( + members: [ + Member(userId: 'user123'), + Member(userId: 'user456'), + ], + ), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'group_avatar_0'); + }, + ); + + testGoldens( + 'golden test for the name "demo"', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: 100, + height: 100, + child: GroupAvatar( + members: [ + Member(userId: 'user123'), + Member(userId: 'user456'), + ], + ), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'group_avatar_1'); + }, + ); + + testGoldens( + 'control special character test', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: 100, + height: 100, + child: GroupAvatar( + members: [ + Member(userId: 'user123'), + Member(userId: 'user456'), + ], + ), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'group_avatar_3'); + }, + ); + + testGoldens( + 'control special character test 2', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: 100, + height: 100, + child: GroupAvatar( + members: [ + Member(userId: 'user123'), + Member(userId: 'user456'), + ], + ), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'group_avatar_3'); + }, + );*/ +} diff --git a/packages/stream_chat_flutter/test/src/avatars/user_avatar_test.dart b/packages/stream_chat_flutter/test/src/avatars/user_avatar_test.dart new file mode 100644 index 00000000..6f036a27 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/avatars/user_avatar_test.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../mocks.dart'; + +void main() { + late MockClient client; + late MockUser user; + + setUpAll(() { + client = MockClient(); + user = MockUser(); + + when(() => user.name).thenReturn('user123'); + when(() => user.id).thenReturn('123'); + }); + + testWidgets( + 'control test', + (WidgetTester tester) async { + when(() => user.online).thenReturn(true); + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + streamChatThemeData: StreamChatThemeData.light(), + child: Builder(builder: (context) { + return Scaffold( + body: Center( + child: StreamUserAvatar( + user: user, + ), + ), + ); + }), + ), + ), + ); + + expect(find.byType(StreamUserAvatar), findsOneWidget); + }, + ); + + testGoldens( + 'golden test for online user "user123"', + (WidgetTester tester) async { + when(() => user.online).thenReturn(true); + await tester.pumpWidget( + MaterialApp( + builder: (context, child) { + return StreamChatConfiguration( + data: StreamChatConfigurationData(), + child: child!, + ); + }, + home: StreamChatTheme( + data: StreamChatThemeData.light(), + child: Builder( + builder: (context) { + return Scaffold( + body: Center( + child: StreamUserAvatar( + user: user, + ), + ), + ); + }, + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'user_avatar_0'); + }, + ); + + testGoldens( + 'golden test for offline user "user123"', + (WidgetTester tester) async { + when(() => user.online).thenReturn(false); + await tester.pumpWidget( + MaterialApp( + builder: (context, child) { + return StreamChatConfiguration( + data: StreamChatConfigurationData(), + child: child!, + ); + }, + home: StreamChatTheme( + data: StreamChatThemeData.light(), + child: Builder( + builder: (context) { + return Scaffold( + body: Center( + child: StreamUserAvatar( + user: user, + ), + ), + ); + }, + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'user_avatar_1'); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/bottom_sheets/attachment_modal_sheet_test.dart b/packages/stream_chat_flutter/test/src/bottom_sheets/attachment_modal_sheet_test.dart new file mode 100644 index 00000000..3f969b67 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/bottom_sheets/attachment_modal_sheet_test.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + group('AttachmentModalSheet tests', () { + testWidgets('Appears on tap', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder(builder: (context) { + return Center( + child: ElevatedButton( + child: const Text('Show Modal'), + onPressed: () => showModalBottomSheet( + context: context, + builder: (_) => AttachmentModalSheet( + onFileTap: () {}, + onPhotoTap: () {}, + onVideoTap: () {}, + ), + ), + ), + ); + }), + ), + ), + ); + + final button = find.byType(ElevatedButton); + await tester.tap(button); + await tester.pumpAndSettle(); + expect(find.byType(AttachmentModalSheet), findsOneWidget); + expect(find.byType(ListTile), findsNWidgets(4)); + }); + + testWidgets('onPhotoTap works', (tester) async { + var called = 0; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder(builder: (context) { + return Center( + child: AttachmentModalSheet( + onPhotoTap: () => called = 1, + onFileTap: () {}, + onVideoTap: () {}, + ), + ); + }), + ), + ), + ); + + expect(find.byType(AttachmentModalSheet), findsOneWidget); + final photoTile = find.widgetWithIcon(ListTile, Icons.image); + expect(photoTile, findsOneWidget); + await tester.tap(photoTile); + await tester.pumpAndSettle(); + expect(called, 1); + }); + + testWidgets('onVideoTap works', (tester) async { + var called = 0; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder(builder: (context) { + return Center( + child: AttachmentModalSheet( + onPhotoTap: () {}, + onVideoTap: () => called = 1, + onFileTap: () {}, + ), + ); + }), + ), + ), + ); + + expect(find.byType(AttachmentModalSheet), findsOneWidget); + final videoTile = find.widgetWithIcon(ListTile, Icons.video_library); + expect(videoTile, findsOneWidget); + await tester.tap(videoTile); + await tester.pumpAndSettle(); + expect(called, 1); + }); + + testWidgets('onFileTap works', (tester) async { + var called = 0; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder(builder: (context) { + return Center( + child: AttachmentModalSheet( + onPhotoTap: () {}, + onVideoTap: () {}, + onFileTap: () => called = 1, + ), + ); + }), + ), + ), + ); + + expect(find.byType(AttachmentModalSheet), findsOneWidget); + final fileTile = find.widgetWithIcon(ListTile, Icons.insert_drive_file); + expect(fileTile, findsOneWidget); + await tester.tap(fileTile); + await tester.pumpAndSettle(); + expect(called, 1); + }); + + testGoldens( + 'golden test for AttachmentModalSheet', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder(builder: (context) { + return Center( + child: AttachmentModalSheet( + onPhotoTap: () {}, + onVideoTap: () {}, + onFileTap: () {}, + ), + ); + }), + ), + ), + ); + + await screenMatchesGolden(tester, 'attachment_modal_sheet_0'); + }, + ); + }); +} diff --git a/packages/stream_chat_flutter/test/src/bottom_sheets/edit_message_sheet_test.dart b/packages/stream_chat_flutter/test/src/bottom_sheets/edit_message_sheet_test.dart new file mode 100644 index 00000000..828c40d1 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/bottom_sheets/edit_message_sheet_test.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../mocks.dart'; + +void main() { + group('EditMessageSheet tests', () { + const methodChannel = + MethodChannel('dev.fluttercommunity.plus/connectivity_status'); + setUp(() { + methodChannel.setMockMethodCallHandler((MethodCall methodCall) async { + if (methodCall.method == 'listen') { + try { + await ServicesBinding.instance.defaultBinaryMessenger + .handlePlatformMessage( + methodChannel.name, + methodChannel.codec.encodeSuccessEnvelope('wifi'), + (_) {}, + ); + } catch (e) { + print(e); + } + } + }); + }); + + testWidgets('appears on tap', (tester) async { + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChat( + client: MockClient(), + child: child, + ), + home: Scaffold( + body: Builder( + builder: (context) { + return Center( + child: ElevatedButton( + child: const Text('Show Modal'), + onPressed: () => showModalBottomSheet( + context: context, + builder: (_) => EditMessageSheet( + channel: MockChannel(), + message: Message(id: 'msg123', text: 'Hello World!'), + ), + ), + ), + ); + }, + ), + ), + ), + ); + + final button = find.byType(ElevatedButton); + await tester.tap(button); + await tester.pumpAndSettle(); + expect(find.byType(EditMessageSheet), findsOneWidget); + expect(find.text('Edit Message'), findsOneWidget); + expect(find.byType(StreamMessageInput), findsOneWidget); + }); + + testGoldens( + 'golden test for EditMessageSheet', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChat( + client: MockClient(), + child: child, + ), + home: Scaffold( + body: Center( + child: EditMessageSheet( + channel: MockChannel(), + message: Message(id: 'msg123', text: 'Hello World!'), + ), + )), + ), + ); + + await screenMatchesGolden(tester, 'edit_message_sheet_0'); + }, + ); + + tearDown(() { + methodChannel.setMockMethodCallHandler(null); + }); + }); +} diff --git a/packages/stream_chat_flutter/test/src/bottom_sheets/error_alert_sheet_test.dart b/packages/stream_chat_flutter/test/src/bottom_sheets/error_alert_sheet_test.dart new file mode 100644 index 00000000..4e6f8b73 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/bottom_sheets/error_alert_sheet_test.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../mocks.dart'; + +void main() { + group('ErrorAlertSheet tests', () { + const methodChannel = + MethodChannel('dev.fluttercommunity.plus/connectivity_status'); + setUp(() { + methodChannel.setMockMethodCallHandler((MethodCall methodCall) async { + if (methodCall.method == 'listen') { + try { + await ServicesBinding.instance.defaultBinaryMessenger + .handlePlatformMessage( + methodChannel.name, + methodChannel.codec.encodeSuccessEnvelope('wifi'), + (_) {}, + ); + } catch (e) { + print(e); + } + } + }); + }); + + testWidgets('appears on error', (tester) async { + void failFunction() => throw Exception('Something went wrong'); + + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChat( + client: MockClient(), + child: child, + ), + home: Scaffold( + body: Builder( + builder: (context) { + return Center( + child: ElevatedButton( + child: const Text('Show Modal'), + onPressed: () { + try { + failFunction(); + } catch (e) { + showModalBottomSheet( + context: context, + builder: (_) => ErrorAlertSheet( + errorDescription: e.toString(), + ), + ); + } + }, + ), + ); + }, + ), + ), + ), + ); + + final button = find.byType(ElevatedButton); + await tester.tap(button); + await tester.pumpAndSettle(); + expect(find.byType(ErrorAlertSheet), findsOneWidget); + expect(find.text('Something went wrong'), findsOneWidget); + }); + + testGoldens( + 'golden test for ErrorAlertSheet', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChat( + client: MockClient(), + child: child, + ), + home: const Scaffold( + body: Center( + child: ErrorAlertSheet( + errorDescription: 'Something went wrong.', + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'error_alert_sheet_0'); + }, + ); + + tearDown(() { + methodChannel.setMockMethodCallHandler(null); + }); + }); +} diff --git a/packages/stream_chat_flutter/test/src/channel_header_test.dart b/packages/stream_chat_flutter/test/src/channel/channel_header_test.dart similarity index 78% rename from packages/stream_chat_flutter/test/src/channel_header_test.dart rename to packages/stream_chat_flutter/test/src/channel/channel_header_test.dart index 6866eae3..c3b79509 100644 --- a/packages/stream_chat_flutter/test/src/channel_header_test.dart +++ b/packages/stream_chat_flutter/test/src/channel/channel_header_test.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat_flutter/src/channel_info.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { testWidgets( @@ -39,12 +38,14 @@ void main() { when(() => clientState.totalUnreadCount).thenAnswer((i) => 1); when(() => clientState.totalUnreadCountStream) .thenAnswer((i) => Stream.value(1)); - when(() => channelState.membersStream).thenAnswer((i) => Stream.value([ - Member( - userId: 'user-id', - user: User(id: 'user-id'), - ) - ])); + when(() => channelState.membersStream).thenAnswer( + (i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ]), + ); when(() => channelState.members).thenReturn([ Member( userId: 'user-id', @@ -52,17 +53,19 @@ void main() { ), ]); - await tester.pumpWidget(MaterialApp( - home: StreamChat( - client: client, - child: StreamChannel( - channel: channel, - child: const Scaffold( - body: StreamChannelHeader(), + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: const Scaffold( + body: StreamChannelHeader(), + ), ), ), ), - )); + ); expect(find.text('test'), findsOneWidget); expect(find.byType(StreamChannelAvatar), findsOneWidget); @@ -98,12 +101,14 @@ void main() { when(() => channelState.unreadCount).thenReturn(1); when(() => channelState.unreadCountStream) .thenAnswer((i) => Stream.value(1)); - when(() => channelState.membersStream).thenAnswer((i) => Stream.value([ - Member( - userId: 'user-id', - user: User(id: 'user-id'), - ) - ])); + when(() => channelState.membersStream).thenAnswer( + (i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ]), + ); when(() => channelState.members).thenReturn([ Member( userId: 'user-id', @@ -118,19 +123,21 @@ void main() { when(() => clientState.totalUnreadCountStream) .thenAnswer((i) => Stream.value(1)); - await tester.pumpWidget(MaterialApp( - home: StreamChat( - client: client, - child: StreamChannel( - channel: channel, - child: const Scaffold( - body: StreamChannelHeader( - showConnectionStateTile: true, + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: const Scaffold( + body: StreamChannelHeader( + showConnectionStateTile: true, + ), ), ), ), ), - )); + ); expect( tester @@ -169,12 +176,14 @@ void main() { when(() => channelState.unreadCount).thenReturn(1); when(() => channelState.unreadCountStream) .thenAnswer((i) => Stream.value(1)); - when(() => channelState.membersStream).thenAnswer((i) => Stream.value([ - Member( - userId: 'user-id', - user: User(id: 'user-id'), - ) - ])); + when(() => channelState.membersStream).thenAnswer( + (i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ]), + ); when(() => channelState.members).thenReturn([ Member( userId: 'user-id', @@ -187,20 +196,22 @@ void main() { when(() => clientState.totalUnreadCountStream) .thenAnswer((i) => Stream.value(1)); - await tester.pumpWidget(MaterialApp( - home: StreamChat( - client: client, - child: StreamChannel( - channel: channel, - showLoading: false, - child: const Scaffold( - body: StreamChannelHeader( - showConnectionStateTile: true, + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + showLoading: false, + child: const Scaffold( + body: StreamChannelHeader( + showConnectionStateTile: true, + ), ), ), ), ), - )); + ); await tester.pump(); @@ -242,12 +253,14 @@ void main() { when(() => channelState.unreadCount).thenReturn(1); when(() => channelState.unreadCountStream) .thenAnswer((i) => Stream.value(1)); - when(() => channelState.membersStream).thenAnswer((i) => Stream.value([ - Member( - userId: 'user-id', - user: User(id: 'user-id'), - ) - ])); + when(() => channelState.membersStream).thenAnswer( + (i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ]), + ); when(() => channelState.members).thenReturn([ Member( userId: 'user-id', @@ -259,24 +272,26 @@ void main() { when(() => clientState.totalUnreadCountStream) .thenAnswer((i) => Stream.value(1)); - await tester.pumpWidget(MaterialApp( - home: StreamChat( - client: client, - child: StreamChannel( - channel: channel, - child: const Scaffold( - body: StreamChannelHeader( - leading: Text('leading'), - subtitle: Text('subtitle'), - actions: [ - Text('action'), - ], - title: Text('title'), + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: const Scaffold( + body: StreamChannelHeader( + leading: Text('leading'), + subtitle: Text('subtitle'), + actions: [ + Text('action'), + ], + title: Text('title'), + ), ), ), ), ), - )); + ); expect(find.text('test'), findsNothing); expect(find.byType(StreamBackButton), findsNothing); @@ -318,12 +333,14 @@ void main() { when(() => channelState.unreadCount).thenReturn(1); when(() => channelState.unreadCountStream) .thenAnswer((i) => Stream.value(1)); - when(() => channelState.membersStream).thenAnswer((i) => Stream.value([ - Member( - userId: 'user-id', - user: User(id: 'user-id'), - ) - ])); + when(() => channelState.membersStream).thenAnswer( + (i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ]), + ); when(() => channelState.members).thenReturn([ Member( userId: 'user-id', @@ -333,27 +350,30 @@ void main() { when(() => client.wsConnectionStatusStream) .thenAnswer((_) => Stream.value(ConnectionStatus.disconnected)); - await tester.pumpWidget(MaterialApp( - home: StreamChat( - client: client, - child: StreamChannel( - channel: channel, - child: const Scaffold( - body: StreamChannelHeader( - showTypingIndicator: false, - showBackButton: false, + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: const Scaffold( + body: StreamChannelHeader( + showTypingIndicator: false, + showBackButton: false, + ), ), ), ), ), - )); + ); expect(find.byType(StreamBackButton), findsNothing); expect( - tester - .widget(find.byType(StreamChannelInfo)) - .showTypingIndicator, - false); + tester + .widget(find.byType(StreamChannelInfo)) + .showTypingIndicator, + false, + ); expect( tester .widget(find.byType(StreamInfoTile)) @@ -389,12 +409,14 @@ void main() { when(() => channelState.unreadCount).thenReturn(1); when(() => channelState.unreadCountStream) .thenAnswer((i) => Stream.value(1)); - when(() => channelState.membersStream).thenAnswer((i) => Stream.value([ - Member( - userId: 'user-id', - user: User(id: 'user-id'), - ) - ])); + when(() => channelState.membersStream).thenAnswer( + (i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ]), + ); when(() => channelState.members).thenReturn([ Member( userId: 'user-id', @@ -411,21 +433,23 @@ void main() { var imageTapped = false; var titleTapped = false; - await tester.pumpWidget(MaterialApp( - home: StreamChat( - client: client, - child: StreamChannel( - channel: channel, - child: Scaffold( - body: StreamChannelHeader( - onBackPressed: () => backPressed = true, - onImageTap: () => imageTapped = true, - onTitleTap: () => titleTapped = true, + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: StreamChannelHeader( + onBackPressed: () => backPressed = true, + onImageTap: () => imageTapped = true, + onTitleTap: () => titleTapped = true, + ), ), ), ), ), - )); + ); await tester.tap(find.byType(StreamBackButton)); await tester.tap(find.byType(StreamChannelAvatar)); diff --git a/packages/stream_chat_flutter/test/src/channel_image_test.dart b/packages/stream_chat_flutter/test/src/channel/channel_image_test.dart similarity index 76% rename from packages/stream_chat_flutter/test/src/channel_image_test.dart rename to packages/stream_chat_flutter/test/src/channel/channel_image_test.dart index 1ec1c166..b22a9182 100644 --- a/packages/stream_chat_flutter/test/src/channel_image_test.dart +++ b/packages/stream_chat_flutter/test/src/channel/channel_image_test.dart @@ -2,10 +2,9 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat_flutter/src/group_avatar.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { testWidgets( @@ -26,19 +25,19 @@ void main() { .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); - await tester.pumpWidget(MaterialApp( - home: StreamChat( - client: client, - child: StreamChannel( - channel: channel, - child: Scaffold( - body: StreamChannelAvatar( - channel: channel, + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: StreamChannelAvatar(channel: channel), ), ), ), ), - )); + ); final image = tester.widget(find.byType(CachedNetworkImage)); @@ -62,19 +61,21 @@ void main() { when(() => channel.name).thenReturn('test'); when(() => channel.imageStream).thenAnswer((i) => Stream.value(null)); when(() => channel.image).thenReturn(null); - when(() => channelState.membersStream).thenAnswer((i) => Stream.value([ - Member( - userId: 'user-id', - user: User(id: 'user-id'), + when(() => channelState.membersStream).thenAnswer( + (i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ), + Member( + userId: 'user-id2', + user: User( + id: 'user-id2', + image: 'testimage', ), - Member( - userId: 'user-id2', - user: User( - id: 'user-id2', - image: 'testimage', - ), - ) - ])); + ) + ]), + ); when(() => channelState.members).thenReturn([ Member( userId: 'user-id2', @@ -88,29 +89,31 @@ void main() { user: User(id: 'user-id'), ) ]); - when(() => clientState.usersStream).thenAnswer((i) => Stream.value({ - 'user-id2': User( - id: 'user-id2', - image: 'testimage', - ), - })); + when(() => clientState.usersStream).thenAnswer( + (i) => Stream.value({ + 'user-id2': User( + id: 'user-id2', + image: 'testimage', + ), + }), + ); when(() => channel.extraData).thenReturn({ 'name': 'test', }); - await tester.pumpWidget(MaterialApp( - home: StreamChat( - client: client, - child: StreamChannel( - channel: channel, - child: Scaffold( - body: StreamChannelAvatar( - channel: channel, + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: StreamChannelAvatar(channel: channel), ), ), ), ), - )); + ); final image = tester.widget(find.byType(CachedNetworkImage)); @@ -161,19 +164,19 @@ void main() { when(() => channelState.membersStream) .thenAnswer((_) => Stream.value(members)); - await tester.pumpWidget(MaterialApp( - home: StreamChat( - client: client, - child: StreamChannel( - channel: channel, - child: Scaffold( - body: StreamChannelAvatar( - channel: channel, + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: StreamChannelAvatar(channel: channel), ), ), ), ), - )); + ); final image = tester.widget(find.byType(StreamGroupAvatar)); @@ -203,20 +206,22 @@ void main() { .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); - await tester.pumpWidget(MaterialApp( - home: StreamChat( - client: client, - child: StreamChannel( - channel: channel, - child: Scaffold( - body: StreamChannelAvatar( - selected: true, - channel: channel, + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: StreamChannelAvatar( + channel: channel, + selected: true, + ), ), ), ), ), - )); + ); expect(find.byKey(const Key('selectedImage')), findsOneWidget); }, diff --git a/packages/stream_chat_flutter/test/src/channel_list_header_test.dart b/packages/stream_chat_flutter/test/src/channel/channel_list_header_test.dart similarity index 99% rename from packages/stream_chat_flutter/test/src/channel_list_header_test.dart rename to packages/stream_chat_flutter/test/src/channel/channel_list_header_test.dart index 532d07d6..6193d08d 100644 --- a/packages/stream_chat_flutter/test/src/channel_list_header_test.dart +++ b/packages/stream_chat_flutter/test/src/channel/channel_list_header_test.dart @@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { testWidgets( diff --git a/packages/stream_chat_flutter/test/src/channel_name_test.dart b/packages/stream_chat_flutter/test/src/channel/channel_name_test.dart similarity index 69% rename from packages/stream_chat_flutter/test/src/channel_name_test.dart rename to packages/stream_chat_flutter/test/src/channel/channel_name_test.dart index 8848207e..01d622ae 100644 --- a/packages/stream_chat_flutter/test/src/channel_name_test.dart +++ b/packages/stream_chat_flutter/test/src/channel/channel_name_test.dart @@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { testWidgets( @@ -27,12 +27,14 @@ void main() { when(() => channelState.unreadCount).thenReturn(1); when(() => channelState.unreadCountStream) .thenAnswer((i) => Stream.value(1)); - when(() => channelState.membersStream).thenAnswer((_) => Stream.value([ - Member( - userId: 'user-id', - user: User(id: 'user-id'), - ) - ])); + when(() => channelState.membersStream).thenAnswer( + (_) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ]), + ); when(() => channelState.members).thenReturn([ Member( userId: 'user-id', @@ -45,26 +47,30 @@ void main() { user: User(id: 'other-user'), ) ]); - when(() => channelState.messagesStream).thenAnswer((i) => Stream.value([ - Message( - text: 'hello', - user: User(id: 'other-user'), - ) - ])); + when(() => channelState.messagesStream).thenAnswer( + (i) => Stream.value([ + Message( + text: 'hello', + user: User(id: 'other-user'), + ) + ]), + ); - await tester.pumpWidget(MaterialApp( - home: StreamChat( - client: client, - child: StreamChannel( - channel: channel, - child: Scaffold( - body: StreamChannelName( - channel: channel, + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: StreamChannelName( + channel: channel, + ), ), ), ), ), - )); + ); expect(find.text('test'), findsOneWidget); }, diff --git a/packages/stream_chat_flutter/test/src/channel_preview_test.dart b/packages/stream_chat_flutter/test/src/channel/channel_preview_test.dart similarity index 77% rename from packages/stream_chat_flutter/test/src/channel_preview_test.dart rename to packages/stream_chat_flutter/test/src/channel/channel_preview_test.dart index a2ab02f1..35d9bda6 100644 --- a/packages/stream_chat_flutter/test/src/channel_preview_test.dart +++ b/packages/stream_chat_flutter/test/src/channel/channel_preview_test.dart @@ -5,7 +5,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { testWidgets( @@ -42,12 +42,14 @@ void main() { when(() => channelState.unreadCount).thenReturn(1); when(() => channelState.unreadCountStream) .thenAnswer((i) => Stream.value(1)); - when(() => channelState.membersStream).thenAnswer((i) => Stream.value([ - Member( - userId: 'user-id', - user: User(id: 'user-id'), - ) - ])); + when(() => channelState.membersStream).thenAnswer( + (i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ]), + ); when(() => channelState.members).thenReturn([ Member( userId: 'user-id', @@ -60,26 +62,30 @@ void main() { user: User(id: 'other-user'), ) ]); - when(() => channelState.messagesStream).thenAnswer((i) => Stream.value([ - Message( - text: 'hello', - user: User(id: 'other-user'), - ) - ])); + when(() => channelState.messagesStream).thenAnswer( + (i) => Stream.value([ + Message( + text: 'hello', + user: User(id: 'other-user'), + ) + ]), + ); - await tester.pumpWidget(MaterialApp( - home: StreamChat( - client: client, - child: StreamChannel( - channel: channel, - child: Scaffold( - body: ChannelPreview( - channel: channel, + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: ChannelPreview( + channel: channel, + ), ), ), ), ), - )); + ); expect(find.text('6/22/2020'), findsOneWidget); expect(find.text('test name'), findsOneWidget); diff --git a/packages/stream_chat_flutter/test/src/context_menu_items/download_menu_item_test.dart b/packages/stream_chat_flutter/test/src/context_menu_items/download_menu_item_test.dart new file mode 100644 index 00000000..599bee11 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/context_menu_items/download_menu_item_test.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:stream_chat_flutter/src/context_menu_items/download_menu_item.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../mocks.dart'; + +void main() { + testWidgets('DownloadMenuItem test', (tester) async { + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChat( + client: MockClient(), + child: child, + ), + home: Scaffold( + body: Center( + child: DownloadMenuItem( + attachment: MockAttachment(), + ), + ), + ), + ), + ); + + expect(find.byType(ListTile), findsOneWidget); + }); + + testGoldens( + 'golden test for DownloadMenuItem', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChatTheme( + data: StreamChatThemeData.light(), + child: child!, + ), + home: Scaffold( + body: Center( + child: DownloadMenuItem( + attachment: MockAttachment(), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'download_menu_item_0'); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/context_menu_items/stream_chat_context_menu_item_test.dart b/packages/stream_chat_flutter/test/src/context_menu_items/stream_chat_context_menu_item_test.dart new file mode 100644 index 00000000..f8b5b28b --- /dev/null +++ b/packages/stream_chat_flutter/test/src/context_menu_items/stream_chat_context_menu_item_test.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:stream_chat_flutter/src/context_menu_items/stream_chat_context_menu_item.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../mocks.dart'; + +void main() { + testWidgets('StreamChatContextMenuItem test', (tester) async { + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChat( + client: MockClient(), + child: child, + ), + home: const Scaffold( + body: Center( + child: StreamChatContextMenuItem(), + ), + ), + ), + ); + + expect(find.byType(ListTile), findsOneWidget); + }); + + testGoldens( + 'golden test for StreamChatContextMenuItem', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChatTheme( + data: StreamChatThemeData.light(), + child: child!, + ), + home: Scaffold( + body: Center( + child: StreamChatContextMenuItem( + leading: const Icon(Icons.download), + title: const Text('Download'), + onClick: () {}, + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'stream_chat_context_menu_item_0'); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/dialogs/channel_info_dialog_test.dart b/packages/stream_chat_flutter/test/src/dialogs/channel_info_dialog_test.dart new file mode 100644 index 00000000..666de07a --- /dev/null +++ b/packages/stream_chat_flutter/test/src/dialogs/channel_info_dialog_test.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat_flutter/src/dialogs/channel_info_dialog.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../mocks.dart'; + +void main() { + late MockClient client; + late MockClientState clientState; + late MockOwnUser user; + late MockChannel channel; + late MockChannelState channelState; + + setUpAll(() { + client = MockClient(); + clientState = MockClientState(); + user = MockOwnUser(); + channel = MockChannel(); + channelState = MockChannelState(); + when(() => client.state).thenReturn(clientState); + when(() => clientState.currentUser).thenReturn(user); + when(() => user.id).thenReturn('1'); + when(() => channel.state).thenReturn(channelState); + when(() => channelState.members).thenReturn([ + Member( + user: User( + id: '1', + ), + ), + Member( + user: User( + id: '2', + ), + ), + ]); + when(() => channel.name).thenReturn('test-channel'); + when(() => channel.id).thenReturn('123456789'); + when(() => channel.isDistinct).thenReturn(true); + when(() => channel.memberCount).thenReturn(2); + when(() => channelState.membersStream).thenAnswer( + (_) => Stream.value([ + Member( + user: User( + id: '1', + ), + ), + Member( + user: User( + id: '2', + ), + ), + ]), + ); + }); + + testWidgets('ChannelInfoDialog shows info and members', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + streamChatThemeData: StreamChatThemeData.light(), + child: Scaffold( + body: Center( + child: ChannelInfoDialog( + channel: channel, + ), + ), + ), + ), + ), + ); + + expect(find.byType(SimpleDialog), findsOneWidget); + expect(find.byType(StreamChannelInfo), findsOneWidget); + expect(find.byType(StreamUserAvatar), findsOneWidget); + }); +} diff --git a/packages/stream_chat_flutter/test/src/dialogs/confirmation_dialog_test.dart b/packages/stream_chat_flutter/test/src/dialogs/confirmation_dialog_test.dart new file mode 100644 index 00000000..db2957b5 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/dialogs/confirmation_dialog_test.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:stream_chat_flutter/src/dialogs/confirmation_dialog.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + testWidgets('ChannelInfoDialog shows info and members', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder(builder: (context) { + return Center( + child: StreamChatTheme( + data: StreamChatThemeData.light(), + child: ConfirmationDialog( + titleText: context.translations + .toggleMuteUnmuteUserText(isMuted: false), + promptText: context.translations + .toggleMuteUnmuteUserQuestion(isMuted: false), + affirmativeText: context.translations + .toggleMuteUnmuteAction(isMuted: false), + onConfirmation: () {}, + ), + ), + ); + }), + ), + ), + ); + + expect(find.byType(AlertDialog), findsOneWidget); + expect(find.text('Mute User'), findsOneWidget); + expect( + find.text('Are you sure you want to mute this user?'), findsOneWidget); + expect(find.text('MUTE'), findsOneWidget); + }); + + testGoldens('golden test for ConfirmationDialog', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + return Center( + child: StreamChatTheme( + data: StreamChatThemeData.light(), + child: ConfirmationDialog( + titleText: context.translations + .toggleMuteUnmuteUserText(isMuted: false), + promptText: context.translations + .toggleMuteUnmuteUserQuestion(isMuted: false), + affirmativeText: context.translations + .toggleMuteUnmuteAction(isMuted: false), + onConfirmation: () {}, + ), + ), + ); + }, + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'confirmation_dialog_0'); + }); +} diff --git a/packages/stream_chat_flutter/test/src/dialogs/delete_message_dialog_test.dart b/packages/stream_chat_flutter/test/src/dialogs/delete_message_dialog_test.dart new file mode 100644 index 00000000..7dbe2c32 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/dialogs/delete_message_dialog_test.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:stream_chat_flutter/src/dialogs/delete_message_dialog.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + testWidgets('DeleteMessageDialog', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + return Center( + child: StreamChatTheme( + data: StreamChatThemeData.light(), + child: const DeleteMessageDialog(), + ), + ); + }, + ), + ), + ), + ); + + expect(find.byType(AlertDialog), findsOneWidget); + expect(find.text('Delete Message'), findsOneWidget); + expect(find.text('DELETE'), findsOneWidget); + }); + + testGoldens('golden test for DeleteMessageDialog', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + return Center( + child: StreamChatTheme( + data: StreamChatThemeData.light(), + child: const DeleteMessageDialog(), + ), + ); + }, + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'delete_message_dialog_0'); + }); +} diff --git a/packages/stream_chat_flutter/test/src/dialogs/message_dialog_test.dart b/packages/stream_chat_flutter/test/src/dialogs/message_dialog_test.dart new file mode 100644 index 00000000..1e2c1490 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/dialogs/message_dialog_test.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:stream_chat_flutter/src/dialogs/message_dialog.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + testWidgets('MessageDialog shows default info', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + return Center( + child: StreamChatTheme( + data: StreamChatThemeData.light(), + child: const MessageDialog(), + ), + ); + }, + ), + ), + ), + ); + + expect(find.byType(AlertDialog), findsOneWidget); + expect(find.text('Something went wrong'), findsOneWidget); + expect(find.text('OK'), findsOneWidget); + }); + + testWidgets('MessageDialog shows custom info', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + return Center( + child: StreamChatTheme( + data: StreamChatThemeData.light(), + child: const MessageDialog( + titleText: 'Message', + messageText: 'Message body', + ), + ), + ); + }, + ), + ), + ), + ); + + expect(find.byType(AlertDialog), findsOneWidget); + expect(find.text('Message'), findsOneWidget); + expect(find.text('Message body'), findsOneWidget); + expect(find.text('OK'), findsOneWidget); + }); + + testGoldens('golden test for default MessageDialog', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + return Center( + child: StreamChatTheme( + data: StreamChatThemeData.light(), + child: const MessageDialog(), + ), + ); + }, + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'message_dialog_0'); + }); + + testGoldens('golden test for custom MessageDialog', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + return Center( + child: StreamChatTheme( + data: StreamChatThemeData.light(), + child: const MessageDialog( + titleText: 'Message', + messageText: 'Message body', + ), + ), + ); + }, + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'message_dialog_1'); + }); + + testGoldens('golden test for custom MessageDialog with no body', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + return Center( + child: StreamChatTheme( + data: StreamChatThemeData.light(), + child: const MessageDialog( + titleText: 'Message', + ), + ), + ); + }, + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'message_dialog_2'); + }); +} diff --git a/packages/stream_chat_flutter/test/src/emoji_test.dart b/packages/stream_chat_flutter/test/src/emoji_test.dart deleted file mode 100644 index ce63faa9..00000000 --- a/packages/stream_chat_flutter/test/src/emoji_test.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:stream_chat_flutter/src/emoji/emoji.dart'; - -void main() { - group('src/emoji', () { - test('${Emoji.byShortName} should bring the correct emoji', () { - final resultEmoji = Emoji.byShortName('smiley'); - expect(resultEmoji, _mockEmoji); - }); - }); -} - -final _mockEmoji = Emoji( - name: 'grinning face with big eyes', - char: '\u{1F603}', - shortName: 'smiley', - emojiGroup: EmojiGroup.smileysEmotion, - emojiSubgroup: EmojiSubgroup.faceSmiling, - keywords: [ - 'face', - 'mouth', - 'open', - 'smile', - 'uc6', - 'smiley', - 'happy', - 'silly', - 'laugh', - 'good', - 'smile', - 'teeth', - 'fun', - 'smileys', - 'mood', - 'emotion', - 'emotions', - 'emotional', - 'hooray', - 'cheek', - 'cheeky', - 'excited', - 'feliz', - 'heureux', - 'cheerful', - 'delighted', - 'ecstatic', - 'elated', - 'glad', - 'joy', - 'merry', - 'funny', - 'laughing', - 'lol', - 'rofl', - 'lmao', - 'lmfao', - 'hilarious', - 'ha', - 'haha', - 'chuckle', - 'comedy', - 'giggle', - 'hehe', - 'joyful', - 'laugh out loud', - 'rire', - 'tee hee', - 'jaja', - 'good job', - 'nice', - 'well done', - 'bravo', - 'congratulations', - 'congrats', - 'smiles', - 'dentist', - ':-D', - '=D' - ]); diff --git a/packages/stream_chat_flutter/test/src/full_screen_media_test.dart b/packages/stream_chat_flutter/test/src/full_screen_media/full_screen_media_test.dart similarity index 75% rename from packages/stream_chat_flutter/test/src/full_screen_media_test.dart rename to packages/stream_chat_flutter/test/src/full_screen_media/full_screen_media_test.dart index 7a815888..4fafdbf3 100644 --- a/packages/stream_chat_flutter/test/src/full_screen_media_test.dart +++ b/packages/stream_chat_flutter/test/src/full_screen_media/full_screen_media_test.dart @@ -4,7 +4,7 @@ import 'package:mocktail/mocktail.dart'; import 'package:photo_view/photo_view.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { testWidgets( @@ -23,18 +23,22 @@ void main() { when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); + when(() => channel.extraDataStream).thenAnswer( + (i) => Stream.value({ + 'name': 'test', + }), + ); when(() => channel.extraData).thenReturn({ 'name': 'test', }); - when(() => channelState.membersStream).thenAnswer((i) => Stream.value([ - Member( - userId: 'user-id', - user: User(id: 'user-id'), - ) - ])); + when(() => channelState.membersStream).thenAnswer( + (i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ]), + ); when(() => channelState.members).thenReturn([ Member( userId: 'user-id', @@ -47,21 +51,24 @@ void main() { user: User(id: 'other-user'), ) ]); - when(() => channelState.messagesStream).thenAnswer((i) => Stream.value([ - Message( - text: 'hello', - user: User(id: 'other-user'), - ) - ])); + when(() => channelState.messagesStream).thenAnswer( + (i) => Stream.value([ + Message( + text: 'hello', + user: User(id: 'other-user'), + ) + ]), + ); when(() => channelState.typingEvents).thenAnswer((i) => { User(id: 'other-user', extraData: const {'name': 'demo'}): Event(type: EventType.typingStart), }); - when(() => channelState.typingEventsStream) - .thenAnswer((i) => Stream.value({ - User(id: 'other-user', extraData: const {'name': 'demo'}): - Event(type: EventType.typingStart), - })); + when(() => channelState.typingEventsStream).thenAnswer( + (i) => Stream.value({ + User(id: 'other-user', extraData: const {'name': 'demo'}): + Event(type: EventType.typingStart), + }), + ); final attachment = Attachment( type: 'image', diff --git a/packages/stream_chat_flutter/test/src/gallery/gallery_footer_test.dart b/packages/stream_chat_flutter/test/src/gallery/gallery_footer_test.dart new file mode 100644 index 00000000..e1c1f29a --- /dev/null +++ b/packages/stream_chat_flutter/test/src/gallery/gallery_footer_test.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../mocks.dart'; + +void main() { + late MockClient client; + late MockClientState clientState; + late MockChannel channel; + late MockChannelState channelState; + const methodChannel = + MethodChannel('dev.fluttercommunity.plus/connectivity_status'); + + setUpAll(() { + client = MockClient(); + clientState = MockClientState(); + channel = MockChannel(); + channelState = MockChannelState(); + final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + + when(() => client.state).thenReturn(clientState); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); + when(() => channel.lastMessageAt).thenReturn(lastMessageAt); + when(() => channel.state).thenReturn(channelState); + when(() => channel.client).thenReturn(client); + when(() => channel.isMuted).thenReturn(false); + when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); + when(() => channel.extraDataStream).thenAnswer( + (i) => Stream.value({ + 'name': 'test', + }), + ); + when(() => channel.extraData).thenReturn({ + 'name': 'test', + }); + }); + + setUp(() { + methodChannel.setMockMethodCallHandler((MethodCall methodCall) async { + if (methodCall.method == 'listen') { + try { + await ServicesBinding.instance.defaultBinaryMessenger + .handlePlatformMessage( + methodChannel.name, + methodChannel.codec.encodeSuccessEnvelope('wifi'), + (_) {}, + ); + } catch (e) { + print(e); + } + } + }); + }); + + testWidgets( + 'it should show channel typing', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: WillPopScope( + onWillPop: () async => false, + child: const Scaffold( + body: StreamGalleryFooter( + mediaAttachmentPackages: [], + ), + ), + ), + ), + ), + ), + ); + + expect(find.byType(StreamSvgIcon), findsNWidgets(2)); + }, + ); + + testGoldens('golden test for GalleryFooter', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: WillPopScope( + onWillPop: () async => false, + child: const Scaffold( + body: StreamGalleryFooter( + mediaAttachmentPackages: [], + ), + ), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'gallery_footer_0'); + }); + + tearDown(() { + methodChannel.setMockMethodCallHandler(null); + }); +} diff --git a/packages/stream_chat_flutter/test/src/gallery/gallery_header_test.dart b/packages/stream_chat_flutter/test/src/gallery/gallery_header_test.dart new file mode 100644 index 00000000..63973770 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/gallery/gallery_header_test.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../mocks.dart'; + +void main() { + late MockClient client; + late MockClientState clientState; + late MockChannel channel; + late MockChannelState channelState; + const methodChannel = + MethodChannel('dev.fluttercommunity.plus/connectivity_status'); + + setUpAll(() { + client = MockClient(); + clientState = MockClientState(); + channel = MockChannel(); + channelState = MockChannelState(); + final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); + + when(() => client.state).thenReturn(clientState); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); + when(() => channel.lastMessageAt).thenReturn(lastMessageAt); + when(() => channel.state).thenReturn(channelState); + when(() => channel.client).thenReturn(client); + when(() => channel.isMuted).thenReturn(false); + when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); + when(() => channel.extraDataStream).thenAnswer( + (i) => Stream.value({ + 'name': 'test', + }), + ); + when(() => channel.extraData).thenReturn({ + 'name': 'test', + }); + }); + + setUp(() { + methodChannel.setMockMethodCallHandler((MethodCall methodCall) async { + if (methodCall.method == 'listen') { + try { + await ServicesBinding.instance.defaultBinaryMessenger + .handlePlatformMessage( + methodChannel.name, + methodChannel.codec.encodeSuccessEnvelope('wifi'), + (_) {}, + ); + } catch (e) { + print(e); + } + } + }); + }); + + testWidgets( + 'it should show channel typing', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: WillPopScope( + onWillPop: () async => false, + child: Scaffold( + appBar: StreamGalleryHeader( + attachment: MockAttachment(), + message: Message(), + ), + ), + ), + ), + ), + ), + ); + + expect(find.byType(StreamSvgIcon), findsNWidgets(2)); + }, + ); + + testGoldens('golden test for GalleryHeader', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: WillPopScope( + onWillPop: () async => false, + child: Scaffold( + appBar: StreamGalleryHeader( + userName: 'User', + sentAt: DateTime.now().toIso8601String(), + message: Message(), + attachment: MockAttachment(), + ), + ), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'gallery_header_0'); + }); + + tearDown(() { + methodChannel.setMockMethodCallHandler(null); + }); +} diff --git a/packages/stream_chat_flutter/test/src/goldens/attachment_button_0.png b/packages/stream_chat_flutter/test/src/goldens/attachment_button_0.png new file mode 100644 index 0000000000000000000000000000000000000000..2294ea844d5a2c166be1e6bce2df662a287362fb GIT binary patch literal 24553 zcmeHP`CAj`7M@ncs0gA+Q9%^k1x3p)n+hUTuoMGf2@ow-KtPHCWQl2|tO~uYf)@l@ zxdlx^2q2&dn?c~-@*uS_i4s|qD-`UN&lZ4Fh2i#xmX-~UnXZr z{?gfD?dr{|AqZOQc+&1G2wLR_K`U`MC2-^se`Y!O=acZS9F9TEXIg#W&GPVLj%RV; z6^r{i34*pkj&?`SM%{hiUK{j!QoCuS();=@$KwL(v0b4a5AS3MuCMI==^FRc?wH{p zpCrZG{-LG+@2?lsRDQctas0EjKiGcjb$G>h2?u_C>6~yR>BX;)-TvV}J@#k&WN&v# zyFsT=XZXJokL;EUyH|iyOJCaIM*n>AWH3bgOkwbu$E8I%Ic!d)a6;Lw%Tu&$aZ#7$ z5M}tNWTLFKzilR3C;O@)!-D(vWqqKip-KX+(zm(Y#uH-;#;NnKg4t5l`K=D|@-@)3L_8x&6C8(O699`tu)f z`;(;}^b{WazHGa}V#++&BObaQs*w!zYO*$v#uE{^js_m)dR~zFU5WC`w~H@NnF@4{ z=f@#fDh1zG6YPVNbE`xk)vUtX5uBdF$qlYoBxks~`35WN1{_*Dg14Vhh8n3{4Ni{j zfP{A6dnY&Wb2v0|DiYdrM_tANncsDY-!C$Ot-931QN+VVgY_ixA=pFt8RY1FqB@Ce zUXzvWKp1-cXgfFXN!ulpBLOimA*8Q|0~ZfJxZd7wk?TyS>5T9LcJJOjUYV#W_U(*y z^*VBd92r_X;F#a_sRHpA;!M`OL^5d{H%@zpPxbQhqT9sIJm|%3K}tyVy_?4J(b_5) zot=!?NNUN`QzscC=|Y7`tVhZS*mt*c&$%Edn?UCN;T5CtBx~V_~IWUV+Z%GK5{DW?{TTT<%GrbFFWe$_aJZAts}YYL|om$twC9S zhYNa*EWK>6%NRkbUAI1o)##`I9y+DV-K>Kc>;D56vG#Viz7oj z)Ja6YV3X{ehgOU9h7$S}B@*XsG3`(Hjddxn5KJ(BkXYsj@)pJlqr;*IidyBoMQX+P zqwf^KCU!PhGXAFChsd32nPKiKf)U32t&*LSA~ z-#kL{S!))zO5E^)o3ym5GV|-dBtPk2RbKNwj3}dHzvvt|xqT2u@#YOX$FmqUJ$CF`U8oL&BNH z6QVK=lep>9T$`vb52~mJ1$|REI5<#l)%T}2(zCf1ljdCa;} zN5HJ3CbQg7U@qIu_0&edyg|hU8KaEX>oVSI)5HEqFb&=|89mW089K0jda5tA9vlQJj#~-YGco0s?B?Z^^8AR0-_t z2#Bk$)s0)xVea1)9*^iK#Jt2%%Rt(QkaFk*%GfWUQ3GxK+h$j^aYl᧾U=rjBI zKRZFVw(wWJg}HSIjJv6+SR$&V)1KXAp@RP+Cyhm!nh?!==&0`S=FSYhDw~<<<1I+6 zx1djbTihKU-)R-&wlFt0Dx9~`CKa_@DEqk}adF22bzf0z+{(+ViIEeNL2>8v2Rw?e zy2g^*1kcitG%vad^%(YV>E>tFDYX1%|A+AS%;aJk5}WNBu-ze=)X)%CLI!~D`CZP2jml*10-C$*?`QlNPRteDMdb-8p zQ6;jt0CqNqyXpIoZu;XW9k5P&iDjDS1$bM84&%zp!@3N7!;*7$GuM-f4lhZ`>PI^Q zJ#9(iXS{cD19g|KDm@v`Kf5Hd=$?Bx`hTWqv_SZ}Xf$EGD3a8j=2@rv{UR&V3VCg9 z9n8tlPG)hQb?}wzC_svMYHnLBkKz`M#sq!6I1#dWhN6pFaHzH(^YKD=n`XR;E=utX zHJu#Lo%aqjnlle=G+we=0iVi^Y?Y=(Rbna~_8jr49&1m*fKi5Fh4C*E9KpR&* z5us01x8eXgN+@)s038;KI=W=__kh_50jQ_Ui6pw3%w25 zb3-_!_n~7bmp>wnf-T38fv_gPngDA82x|hY39u%BuqMEo;3I2-`Ff_d^k#$dhsxag z!ME~{MSklQw<(}q$lVC+u>8XWhKURatRb+5fG~xSHvz_mkBki`E!lU$1z$P~@R6m4 zrGx2~yaymmw=gzfY{1w65Wu7WlLAZ%Fe$*K;QxICEUEg)*WYdtX&ADHUO-2|>p1+U z{=2oKKPxFIZQi_jH5gxBy_$AxaM^>1S5_Ik>h8g3*&H`KO*ilSw9!JKjvf!8z=eU9fkiCY{%(-dy_nwX3@QC6nw?8+ggoo`vao$?P?o zS?R>V@SmmnBLkvJ&888j&i&1YD?~->;2W(}RoTpRSLNqUuCcQd20d4uYx4uLUWQb< zs5dM-1J9>JLRpdK+qRLR!Mp=(p(P8Dn1pgmELrxAb?!GC*{7_g3U~2GxM3^yqfT=f|o;+>6W`-A9oc9 zBv@naD5GI)1;gI-c_x7Bt z6cr5;Gu{ox8^C`5Et`QI?o&yu?{ACX#PK*09O4UdTzIf%f@!t%Ry9R7x>r}YIbnN2 z0Q<3kOuXfPb^MiCnoV4<=R@Px(f&|)AjGyYFE0*u3e86vG?!XPKvge_Cgv=ak87zU z5;AtC+)EmW;6%6k;qfkbe#^_i+}``N{zKQKHVdPtr8AUWyso*a1fQOl;0sH(?q!g2l;x`c8qywgy5=vx!m zb(DdOYkB7kXemj5u?OcC0La~D9k1Op-t`|56r69LRe={)$p*Qos-|h(foJ(fP5=jr zwU&zMtHltmqWasK~h%R50uu8~|ar41#+7=Jie8BR!rT z8P6*Qrl&8PJuTHYoeZfI@jD6Gl#OccLvY(s9pu#`Q8e&YzI)Bmy8G4XZ07m0SFgRd zBy4XCt9l$^PLvjLt85IvF7Z*MaaZ&Gz~C?3?4E{Ha;B%N`#kMk7(O1-uOez?&mYK^ zy}SMWtG#Yyc~Y3p;y0>I?{yosbm*Kk6#^bt1aByW ziABwm4S^013JQET+J_lS_8n>HkFsn8*$4$peRv37kpwfXshfU}7URmG(L8g`8K<3B zuD={!#1sQ2g$06@P?UUJb*RU{ec4u(ME+j4OxIE$&ur#K(#%G9#n4b|t2O)swBS=* zz4@|MlUa_l=_KfWu(satsgV<$NGiada$c)xPxE1u?3R9V0&Fm>A)`!3gBMe&%CsRc zc!m!O-nl>JZ6I4vy>ahKlnczexD~C$RaRXFK8xkc*VKxoXx}UfZxf2pz`1?{vIcurU$K?t*L!ntbdAE z3|mBL$_S@grh(^^<2K@p8eArsO5Pd%Yfi1e^&&NdF ziQzpskmFp)k81^)T5L_HyWoZ7-u){I13ashN|@r8mrP8Bu*o*rlyqG^!>Nu&>o1H+ zxi{r-fBX_qhzA~Iq65#Ux3~TcE81EflaVZ@p^#u2|2G=#8F1e=G)eE|+@K|p2y3g_ zDymn@Vo{S{4KxJI&n$DCK?K0peZX6@t?957s1b!$J-@&)T$VllQOC(H=$l(84Sx!O7X@lHjsA-UV9{^RE_p$zO($vI3&=`J zGSgC_|6I~mVda3AF>CU+|E6G3!TeVE)Ki-_NOw$8ci7BO%KW#b{Aw}O6m~;G(V3M{ zjBEBaf9ThSHEAfMa`sgT+=AO=v&8_h6(CTeQzLj1r8k+dDbs~-mbQSaI?!4cA-zCF zEtl8NRY(R(A3pHG;>o=|Z;NZ^Equ36X#|UW?D2{9Byw?|I~+=tseE~PM0J;ulY7Zo zkBpJAv9SV20#Voy!LhPYtzQR(ZjhKN1K360*=Ko1^fx?&TA2rZ?}^zQ9xL~JQS+l9 zj|_gFyS|1`K$uYY*s-sQ|wl35Ig-0_%7rHkXv~?*B4|- z&nO(3ZGv<|M~xh9!|i|D^j}&*z&#p`P7TZ|xYKJ6RI~r~_13nEn*$NE&An2c6v`!) z*|DA^Nh#IuZq7UiE+m|J>2=9_rn^iP4yLZw%S9bPtM0_B5{J%>3w_`2hmTv|T_xS8 z#NGkv&q}N_{I9+|LZ_v)hkS;|TfNVL*9?2gw*){>o=?V&#Vs^%cjFnAw1+s=z(IHM zWQk6xw{8b=cc^tVyGCT>WaPAgOswU>kL*ERWLe>2*#TKV!|oL91^cBitzv zO3qzAFuKDcji(@AJ8GetA1H&j_(*rUTz~5Ly$e_T+N#reoH3ZfW*l?Ac446F1 zfxzSulSgc`u&4?c8!$FtY{1w65Wu7WlLAZ%{@zj$C`^-1EGkWVwUQ#k7JotQ&&cn) yGn7r4NMXVb%en=_M9vzlA+UykFgAQdY*_K@o5bZf)uMhlCr5i{JLWOJ@BatVu0cKk literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/attachment_modal_sheet_0.png b/packages/stream_chat_flutter/test/src/goldens/attachment_modal_sheet_0.png new file mode 100644 index 0000000000000000000000000000000000000000..8c4b011c5299baef846af000ad0a21c5d234dfb0 GIT binary patch literal 49362 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 z zgE(;{v^Ipta_8Rx+$V_%VvoLM_g!se4MQ=$4JY^7eN3@V!&ssw*^W?~S>7AA z=CfcRCLUcEKjt>T7_3ZypspaqKh)RZ_n>Fd$^=x8Tgf2CIc?plOt?S@9a;DHK z`1xaQEJ{j<6a7+}fTep}Yv+B}HmPq6=nZgRLDf;Kj*$P{>Ag3u8##{`l}-SmAJD%#J@^`F_{qtEU8@bepT?qbjKk52bL zU2b&nef*caE+IJ@icHO>rsKP@r7G-hhM)5wD?aPam=1hUJZpE~c#a8H4|`$Jn9MA> zJV)B`=#*M#3*{Gt>oq|^Ye%ia72M_Z{d$*YSkYTN%Qh)z2SoUfvuY&07%J+tLRT(H z90DzWqT41WF0Oi-$!vft01lSnNQnCB8V1Vl;v@T#9{5TzGP|PrBgS?nP#mv(!$cp_ zFee>9Pe_TjXR2w2G8PC!W!Ss6NS|r`;cY{Pq|;_s+`yg|`{JkkGxu?P)S*xaU;023 zN~0M%F0V`FQ&|E+4Po-Q`^B=_!|;J#xpmzlLNb5BiR_l#5Ae_^scNflG?flasDi>5 zYG*7+$e=Td84AW2CUioTQ#GaF+`VsrnaKG;CGv3J*PA8r_KZ(s>hsz3y&u<>e7pkv ziKuDys#Uds!4ASJcU~|46N%Y#OqKY&Obg(ep;^{Ps`gE)pBs@@q~wBJedRVywK5v%!~+K|YI zcDcouAwq`%d)sb*_o3!KZ@i>LGC|8k%QvzI7&y5QvA4CY5ozXlF^!qXD1auW%DQ^Z z-vA(i5r{Mt0k5u{v-bWc^jLB7V+=mF52O$fNi(#I>4>lZlr(2?wl_~+Lum>H9yrI6 z@=vQ7$|Xx8w2-w4#Ygv{n9d_XlJO1l877lm%MUi8ZLQ6(L7%cF$f55{&iwCJeE0$U zTN85$0z90Q=Fge{{I4nf`Bm%oz44bKEWm+Viz)_bwRjB~!TcTseBx?l2W=?4K8*OJ zvFY*x>7iHA`(&3mJz0GAd!UZnAZ-+i^X4eSW^Df9WWxvs4&!MTNBAfjC=ehq)Wrg= zJ#K$E_^8iLC499jvZ^4Ijf z1B^AlrZi7z%g@Q+e%4obHQZ50Tkm_v8B}EY>?RY!*M;Qv3PO8xnF@fba0I*i)hp?h zNK(XlN``8o8-z!hNZ70}V)p&p5c^6`lf?ze(X-x?{$+u{>gdpug#LqPU6smM0U41b zNEo-%VnhrA3NOZ5SM0#B;Mz1qdIb)a;qT*vxiu+yRc1YRl)kkf&_8#dP@HqRl&}E= zosd%L3&pGc!Zp|5>0%MEBk6m1Gg5wO!1QywsK_l!7P=tlocamvBW_LVooAtWT`0ki zA$WR_{&yvgyetPHsBI~(vM(y=wry#A=Xy{OQxqwgdMZ&&e4mWy_B{OWbibj$>3hOU zcSO?y@LIuqR2EKdCG^_9V&fR7vLI41Ad2D+AxYX^DV`<}bS`bt8M>%~gy<^q#II!E zQxLoCe^5E_+u#+KTap`dpf9!|A!Fgl<8S=&n5LC~F!jT~{rUz44qubk7h-rp3@-@5 z@NSTn?cv=pyc>phgLE?phIhm8ZW!JT!@B`jV7L?vmxAF^FkA|TOTjR#!d>wHdt$ug z)gqD3+kKbaF~w`r@{vQhXU+_}AzXFM@NY{B=I}L%ctH&B2I(3Q v4DW{jz1>hfvY-ay5$;N-OI*)K4VLRb|;2UT7=~pf@GmcwoxBp)Oj@&sC literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/clear_input_item_0.png b/packages/stream_chat_flutter/test/src/goldens/clear_input_item_0.png new file mode 100644 index 0000000000000000000000000000000000000000..a6503af8ffb4ee6e3614c7359a6b894bbb5b907a GIT binary patch literal 24076 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 TxNCgC4l;LkaIw$d?|bo|{^Bp( literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/command_button_0.png b/packages/stream_chat_flutter/test/src/goldens/command_button_0.png new file mode 100644 index 0000000000000000000000000000000000000000..72719d491cc4415e63ad752d5d917c560e7c2b14 GIT binary patch literal 23274 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 D5*uQE literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/confirmation_dialog_0.png b/packages/stream_chat_flutter/test/src/goldens/confirmation_dialog_0.png new file mode 100644 index 0000000000000000000000000000000000000000..9a2676adf78817fe28ad0cdb89903e6c93c67f49 GIT binary patch literal 55230 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 literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/countdown_button_0.png b/packages/stream_chat_flutter/test/src/goldens/countdown_button_0.png new file mode 100644 index 0000000000000000000000000000000000000000..e18ed82d86e61e7d57fb09c9ee9ff53e0b15409e GIT binary patch literal 24794 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 V=`!}{oN&l(bAqKAV;lM0{{gX#p#%T` literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/delete_message_dialog_0.png b/packages/stream_chat_flutter/test/src/goldens/delete_message_dialog_0.png new file mode 100644 index 0000000000000000000000000000000000000000..9e7f6a7389377ffa7f7cc7aefd8b2481756e51f9 GIT binary patch literal 68100 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 Jl9xCf`acG$?wtSt literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/dm_checkbox_0.png b/packages/stream_chat_flutter/test/src/goldens/dm_checkbox_0.png new file mode 100644 index 0000000000000000000000000000000000000000..39535158fc5c4a6fc48fd8023cb6a6fd1d51d529 GIT binary patch literal 33581 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 literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/dm_checkbox_1.png b/packages/stream_chat_flutter/test/src/goldens/dm_checkbox_1.png new file mode 100644 index 0000000000000000000000000000000000000000..f07b2a9de2bf0f4336d6ee304fc1e74ae0d6b42f GIT binary patch literal 33436 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 literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/dm_checkbox_2.png b/packages/stream_chat_flutter/test/src/goldens/dm_checkbox_2.png new file mode 100644 index 0000000000000000000000000000000000000000..a5b1eda7593d6b2382ba7b43dc2f74ed62a513c7 GIT binary patch literal 33252 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 K5|3W^=YIjG72eGN literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/download_menu_item_0.png b/packages/stream_chat_flutter/test/src/goldens/download_menu_item_0.png new file mode 100644 index 0000000000000000000000000000000000000000..2b5939181269c033e344dab26895433c85a99de4 GIT binary patch literal 27778 zcmeHwcT|(h*KRx(up1Rns)8MagGeV}p~yi+loIKP1_1$)-ceCPKNdh#Kt)9%K%_`7 zA_SEXM1de(q67#ev=9sgzKIgvu-3h6-9PT{Tlc#w`9qV}nR#dSe)hAUJ?|t&oH?zx zevR-N7!0;v|Ah8g7;LpI493ULzY1I_YGJMbe=YMkt9J~RO%NRfA69rA(>LY^uR#8b zXc%kJ&nlAk%!x=@*Figj6 zHJ0;nU?p78Y8TX5`SvEeavzGRi;G(ncGa;^&J>U2OsZD<(^|8bti7qt$#ro$$mKhs zZa&Oyb`7M=j0}3iDR}D41LS(@5Na;=$%BxY`l?Xrc*e0H{q z#mYwJ^lKwod!VryCEM%{ZnkNNz%mUM=To)AbANTo$w0-u)kAxi338Tmv*{MUbj0rw zq!vf8J<}eI&Hgij4m3h$BcJy$_Tt}S_Rhy%JkITn3Zi{wULFBa4>9Cd{ z$d;>I;^D0a*o!kQgiN}v%tRY`s)oHd>gL78{k^6>8v8JsJ+&^#-nNGm;C2OY_TozP zpsBsG++FRYz@C~{6g0IaeMyY$pK&)q*W(+UZ^U6(x1Y0Cqt9e&vr`KJ@x#SC?AK>C6m?u@=X@d-u`{!#7fF zdIHZ`sH=PVxaExKr}h4|Xz^xBB6g0&VkidDGk%&xSy)(PY6UW1lles;7lztAj>TGw zinmV8FaiYZ8d9aU+9^WIg{7mhMe87AKi*k=!3eUlN;rR%4uqZF_1eg?m_@6=WOr2e zNkXlC*TMzeBo}=jx)P05iGx}T?yBySfo^4agFou)3M6OwSM?=j`k{}THrW~NhK72; zzm}`#b$;hgsOeQ5Nz54J?W)aaEKYyX@Gt|(YGDbev&|VkON1~R{L1@Af9)OAMSV91vVf>jPTtn}a#00|^gXKsa8p}KmEkkTq z^$=H-d*0{IoZ;$hCy8-|=;=ru2sDU+U+Yz%N;DuC3vQ zDb1zIZW|=4A&?wtd>n(_Ff}zq{)QPjzqZty<U>rblgZsV0-LQ1VL(dxK0iB9aF*MZe1x;z*F-tWubX@^metpmaZ)#IIzBHEo6=Kh^L5dKI9V?5g%Z(N zLC9S&!tQE+b;yXLx{{bE$k(B3Be2~kp@o;l^HTyLf7yaiA#%086@d*t1+}(}R^s{mv>uM zGpXP5n9RYi^DIpf?pw!GNFHH5hyc_1+1Ux=oR*kfRs$;U-pg=~Vu6u77lm6vLAjOB z6vH{lK5gWDj^DXJul80#EVRtarbH=>PG6>~;ig3{j~N>G2uqB}$$;i|WG#?8R!S8a zP*lPW4xSB*uQ4-$Ec`^r3XT0To$(LkpnOaNKjpxfNj!ARflws?ecY0cQV*lC32++| zE>CZ8l&m&44K(3$ks1mpn?t0`0}z@%AI$_&C$^+#tT~D;i@2xZ*>*@0V^X{1o}}!} zc3Fp(ED72BCN%D$;|xwpYdmYoX*H_V&}!fQ;Hmt4Bf5z z@Z^xKPho8O*Y@`AY=&D;c226Mi|0))gJgY$n~G>0<>z#oY#k)?cR=D9=AYHT&jssA zYYT3)Z3|P9_oBUxn3x>Gww66Vt6!*}+nUUOM z?ZYhToc3mzyL0rXLrXfV)i3RQP>L(0mUM16S+Xd9NA7rgCBxlywlA049=-SL zxuY*sL6AGlU}WSj?#^ZJI=F9CwNjRQmN2)mm)z6BhE6PLeFH=`6ab+HDhtv0mTxf{ zCmWhBLedX*!tcf=j-z)#nj6LsVIOvkYsh~gpzxaWECb_#fCmB|2w*%A@Ib%=0gML% z9ti$F1Hs&Tg4lf+Z28}t?B{fR#Hk9aB5tfvVLvtjuT5MkSrRG(k8lZTG*s-s?r@1` zuF_STOJARWidEj=iw|T%d1hHG;CN_R-22M1WzFGq#Y+i@sjo8qfcJ4UVQit<0b3=wq(sq(7XiAwg9i1 zUOems<5koD>uTDe!x}nTHGPNgf817#!=BM1Wc5;qeGcFf{J{ci?eQU+mU7Q@$riNN>|#>Y?NNyhnwO$*LZ}x8P_i6PFqPqbhw zgkTp;a}$3RkBY`E>1w&wC2kT0|iF5b-CJU^^ttvA9wWn9@xiz z>oDMyj!$6A1e@;xZ#w*i>2m2TlFu&ge>hFCnSYohp@2GxPu0@epfOri{zjWK<)L2w zV{Lq5furx_FMYaS^!>KTp$UoLX}gmT55G<^<&1koR^9fU=CGa?)96YYcITE04Jin9 z-8gZva4~PB5$zQQ6V2!jkke!iq)ktLHB7TA^>(j;>uEnyav2Kro|zhX<3(v58e7IE zx+z9j@qq7qteE7MR5c(8`swE9aO)V+xHL zTawkhfF){$Y(yIo3`@E%SdM%3>b=_9HIsv1?U|ECBIu4l3$BMF}2^6mct)-*fW8rZ~hD(%96rwZr^?v^42Wop{vhEqiKWl)JZqRvCo_ zSlZ?8GQVgsbtMHx)$)zaOnhH$6zTrC(-B&;-#7Y^GSTQ$kCbZ5{3iOE&l$!5cdOA5 zh=3`4bZW@lFLNzM#NLz-(L%D8$kA?>j<=WkGSTz5PnKTksX%zRjI23J@jRyCk#b$z8+-f7fu@!0`@~C|;ikt(q}+Phs7B2h41r+^ zBGfQ={9e%*PGs$z416AOZM-i^F7}R;9?Dy?K0PDjz)Qnaajp4ZjT1OC_w_->NTw!uJC8tH9^Yo@yk(bc%LY_xi9lGJh^Gb1Co zT>0eLn1Ze2OiGReE{Hi9adU&(t6r8pE;^z%N=T@zqLgiPa%zZmbNMac4ctm0%S|3+ z!a-&d;I+EXPmhz{Z`6|UZ3JF}AHTisqex!-{rf+H`hSsA+06V#wDdT2GFh$Q zl~-_5>gt8Hz3K`Uvn^*1Wh(Z!f=C1BORdICDw3~`XWd~LIyySWsE|o?@EhuZbt+4Z&K@0<|?qIhsDF)wpluv9cSioy>xAy$kx+(<* zR990t98f@%p%&{fjTJIK)1yGTzMQYWT+ZzL`ST}JG*p2Ji+z9nOtdFFR+R_5gGYc^ zi%e2*5TsWe17t3kQ#Sq&iaJe-ivt&NY|1SlT1S1$bEw-GGW{NpptNR1iMST+q^bK1 zx53S{iO^)ub$m@WbsDKp)IttzLFE>fDxnE^&nnZf}$Tf0|ROdjo z!rC6~)=IxrpP=LC=ZB(R=MzUy5Y%(j(9U3zqCt~Q?(-~$9+{A=!06X?E&NXiL_8P`Y#i~fs0>amAzpI?jImrKIn7FL9_ub7j0Q8kPA<1x4`7sj!(hH-# z?*eA?exW|GvipPOJdURmlGW?>r$0VRQCIgII3p$MKlD2N5IZ@mI1Aup`@hUw8BY=a3=tMc2l0Nb1A zC_6Rp+Y+gx-LHbX5TBSxRKdA__;;t5CK06PZAw>=C@PE5(-{5eu;XUHJ=t?!z1R6# znS`4gHB}S{46zSgCKPsFUbSAhx!8JP!nTcVIm82j8k-aVXXS{e1~Uhn@*EoX?ce`8r5=%Tb*!&8 zX$+)An`U#$*#UHwoIQYP(wLE+n2GTRK{z{jQMlq|rrGO7lxl()T|!&-oJ6P2O{TMH z3V1+|p3~*CS$Y>kVT1ZG_F3z~$p_f*_L-nifj10EnK2Lw)G?26(Q7AAk4v|lrP7Z* zJnVSZp8Sd}coa0LMWjO{fX{3u1CV6>@HQ`ykM+`YV-D4IHl%7*`ZHc0w0K)H(Wqq; zfn(D%9n@oy zor-SV8ml&lA`#YaF6abKeRF`r$M6b`#*IKbABa^OdWA--(3{UGI9t5Ea$Z}zY>r7u z@<|OG|EQDW;nr5*+Afh=1e?Q#5B`wwbL!OYn8(jT|avJ<8;P=B4p9+_l< z#svN{u#L9%Tgy4{>jhr}`q}mso-iGW z_&zs7Ym5ewWld6VFK{N}r_1kQ`FLniZ1Pk z^VYUzpP>LTh{vch+A5gdZFb?8aJ0(#;HJj@+t-Ri^gvpViHwXKa@W__$3Ie*V=KVD z$)0D~J_gC@Oy|Hpt~Ye1%+)Xrl6SwMxTSP7Z^`y)uP>ar7f$Mj@V)(l|sTIrm! z%~3*f*(n`}&+TTO!)@{k!N59E-76^#%02jKkNJ&BS|PzG4fnp90iYhL56o9~+RFGS zD=bW@m$JL+KrcAoe+!+{wgoK1(%zMUbxiC`{ui*Wk+I7C88OYl=l9lhm(Gyc#g1z^ zy%k@8ixF|4Hg+jea3XH_=otphhRmFL`{m0Qd{cygfB-v*wEpS@5?cr;3QttF53M`N z-Br}9*+{R2Nw_dKcP%cZ4Dix3P%aS2i{?2VY7%up3UlANPkLw^$fCMod`XUq5=^eN1= z8XsvDfXiR)trAv8jYtQNMcG{Re#c(0rB{o|%HuEC)ejJ@@kylv&!U_y=SjUv~hF9Svx7TfXDD$3Q5$1EE+4YKCokqVC!lt5T%|IUDgx)`*)u z&_+pQw~Al==9Q05JvN1t0avV+E^-&vXGUxouc7Ui5%{??xg zXfuY<6QaegaeICde4hDD^uM|G^AlSH#+>;rIlsRTC;GZIrXUp5W3w!a58YlbBA96H z@0btRS6J!FHnup$KUP(sZScS4-BLQ;7iB*+{H=uqW({@^6#62iv(eV&du-HxaLxkw z>i}sj1Ih?%-|x2`Xobur&P^=f6t-rIt$?GUHef(+A4*oCU=Z!e(VKm29S0)7oaPK_ ztN=FlF#z<4>K7w2~ePY!ZQZHrV|=7 z45f6V(YMwLp;}p=q;qNj0a;Q!vdjx2AGwz77goX)zw1>cKi(3jF@9oCu0^@Om!;p& zc%2;Q_qW%BWDte71dK5|o>&OKb|73>N`oijuzviCaB9P}kQq(s1_bhomoN-pgeyT5_jLCrp6+45BvabSNU zW5Q#DN^dB>NdMj*FU_w-?j-X%I2=gCEU&Us023S=z-)wpaz`*afvw*QySy!cIA6}* z;%F|x0o76q5(T`H*^;xS`zsJnzI_pU`);h2MF+E(m$F$TiTQogooKk!??BjcT4g7V z&Mx}Jd^&%lcxt4>tP#-tyMpZP3(MYzS=`Wjm7AOUio{=QHoW$&YnL#sY~nNq@a#VZ z2ETW12A+m9t@liWx{8!l*c~ZXF)ay1sz|uF|K)twF10}`b?;#`I~B0WRz%q~8st%3 zz)5LFX}UIk$)Gxj0u(kq`hH^?*tKbTVUQ?nB(gAz(U#TNiW?-u9;U5df(Hl~FB*8!0OL`?0tk4~z>5Z6G%R3%mm4^S;3=g)mf+DN zj~@SDrN{e$20bv?I)3*55x^_?|KOCzTRh(SznR6Kc#r)4M!D_3m2^>WA-&($t=a`W zBRvWIsy*-WLayN1flwA|fCvUBHfrfQs}Iih>Y|2%)$4*g%>MP^wB35<>4y zk)j}72!y5}H54Pg-?>nP^W%xRsJ-FRLwpBzx-``THzW!d|c_TdtoqpF$!l- zU2_Z@YA?dIO>vadvUKkpP&m`%dHR60R{HaxraN@qPdthh`CPs~dgpcbWG(yQ57(EN zw#GcqJhOYty_2_fj&AtheW6#)%J-$cKEKLQe|%$RyuN*^uREn(u#?R7{o8_8Xq5@y z26*)Hhr{;xi_h;ytTc0`LPEksl@e3aN&40CV`R^1#&r`UnVOEQ`D>!=SG>xM zJxuC7tGRGsb?m9!J%)nYu~pv5vx>jx^LDQte{t?5S5dy=LB4gnR+e+8?jF5>_wool zzeg~*Oz}-7{T$)k>Ih%bnG6Ma3PYF;CmyVd>y-J?Vr`Sal_PJx>!^mE*8tK(h_hI@A`k8o;rgqwof4e=sqgbbC{zrNAi zVThM33<;Ij9Bs?qS8V={Vj(7N=7J1j`IA0;^Jx5?_I9ZVWkLcMt8h$DEsb}uP8wE-`tsLriGQXBCoe86Oo_TpB=o5H=;-JWzLx6l(C=AW$3 z&e=3JH|4U^uqu*ghoR`|axrmU$=RD%g&nr9*yzftlqDX}`mI_8_ zZ*dp*>eY@6OuMYPfA78L2Crm^fYqya?Jw>+y!t5Jf9ZW1{?=Ss{w)RW!@aA&Zrf&f zg?DwR=kyF~?))D;u5IV4UWIq9?iI0iKv7f9b!b}H?0Uu zWn;6f(QY2^sdQfa@;^mQ)9lstY>{x=L@oVlC{yH7q!FClBrdL3EVXJy7*<#>=jwWo zU$`rJcy*6%KBvb?tS-Oyum3oQm%RV+3MYO3;|@H7{^JKQ9DD!c0POD46Fk-oy^ovd z&h*<@PL_Km&z@Ob2FL#5!8KNH_l%w}x8`O;ZlcwydpcPThPLW~Ff#Du1FLHshbOzN zxtXW$yr$e@Gwe&EwN=vtm59%I;?`Tai&vH8WKEb~^47`8!M+yauogp}{etttNL|z^ z-C}JN9V`P@)Dj~!`g_lDIG4PUp;7W|+4ZBkwl5eNS>&w${m}wNfI_(^;Iv+&uR{@osoAkWz}(duG?mqu|{0$!DB?M z+S10ecN^E((q>HZ@7c?wer#19M=o;{8&`$-*3&y#eT^{J^A&4ztP0cDlO89! z=H~6(#GW-bdwC@Xt+|TqKc3IaS(Ima14%pIgb}n-8)JUVx`}Y(|OMIS-i@;JW!*4%wQmw#! z6P1h0LpLqb-Q1*{_PfT@tzTz$_w*=4J!4vR!L$j$u_@dvlQ0;s4z1Ewc8>1~;nDYkDm8M@dP_ zS|PcMi&Ga`vj_>*e#vVle=*!Y*?oGlb@oR`L5FVf>_|)6?Ce0p(t<48+D8XcyKV1w z%DAX6&CidK7v(q%6RS+pp2j{{K`fYVyWhYqBcnjiVR8!RW*jxFmE@%Ail;PH`>!tZ zraGC2c&>{esj}_2FZUb#VS6{&J1JpxZYLQ4?&ygXCahW|MsVQj-M|tJwN(kfJg4WC zTtA&~adm^ziB0(b{R)H6P_Bs+IodncbxAhKWc2vm}{EXEh1_|5><- z?tx-+?*Cqudf{&4wg1e_x%V|a;p22df;ozd$0tQsoTMkOWXPpK1BWdCxCRQPgEVE` zNlJ>8wY0mlMj(j|OzLbWPSLO2G~P7ON8h>%o-xjeRrG7rdR$*;t+wsCE5^OX;~m+f zB)?X3FN17Xck=C*<2%=C(<$?{&L49pV{q-0Hd7AqtR32s@Nn%|2Z{f=IVxpn?ajUw zNB?uPRONqf7UWqIb}&)+#{b+*QTxxLoXytS+nd8{Z=LT-T`P|-&nnkipXae%Yt0PP zr@4Ra&H3+I*FNa&2BEdH#8}f}*G?A?{|lDAY5<;!62Gi5lkT@Qj#t%PUxj{{ z3r_!TTiNKntC$-@zpDF~(`$x&w5lk7F8?DL%pWEAqXd7H0P{x){wTp8CBXbqf| z|1%|6eBaTY*Q7ON!@i_lp>Q|djDMKvL73pxm=?;b5C!QMTQ43E)O@o@H_}BUD4rWM z$n>Ot<-@3W@!m$JuFIRJA8o#O=WpjDAI$D|Ozj5>}*YOtG4DJ3K}|%<{5(-WPO4|s)q*KZ=un}k6-DNz|2yxRYaxF8_>q;9v*SkrogapvBIdJpmS*;ncY(+)IjO66n2AMl|?ud$rT#1T`lF{?HyeWD1 zC3)K1kA8*q^QGmuoE-Mpv16h7;zy2L?yC+bq?y(cd7vp#Cp6+dzNaD(G{i+hZHZ%W z3RCm1eiy+cP3r+87YY9HJ8%3Rr=hNH?&RboUbaF`t=XiRM{9R_pyB3%0OUcGuHb^F_e$OA~XznFmr6nrHtHgyr)Babv%Wl;%9g_Q66 ziFI{#Az@){-SM;Xo*6z*rp>ENm|!mqI+u_+z2HDhhLnZk-dae{cVG} zp`qdXhNMYF*9?<1dfe^XspsY8ryLU&CE7;UZB3}^A|!aMCr)$`^xKL&a+#6;M4 zdMvpu(8{@_Gdtno+Hoq89VrVD*PWbl3g>@R<~mQ9tEsuo)5<3%Tdg&s>YAVSEiJKQ zVH8bhZ98~@_Pi!EfJ^G+$bs;MO`3Ex~w5TQ}0tO*y6(@V%vMC;+n2#Fr5Hr8g~=0`!p{i!;MpHo-tD@i55( z*@f{2j@D6d105*^Vz$GSjM@3I8m{@pI(e?fk5Boi6J%^>8cRaLNPC_}r0b85xj3Lr z3F+r&51YD9wwQ`aOXrP*ER!w1*x1_IHV)g9_1L($lE4#`Gh31x%^O?NOmS9LBPkim zajI~IsH|+qgO8Ky_Ve4>21N_C(^6BfR#sLrx{vY}va+%!7?=B7;*?>62G&4D?qn?0 zz`0LbOQrOYc*~X?{pn2DN-EonjCIrE7L&IIYDY2Cb5aAsfA17 z&9U;AKyBZs#wjP}O-8>N+RJ1x{4%jj1J}e&)bA#QJ8nU@Bxh~hxG~di!4LSiKYA(| z-4;OrdLbhoCZ?v;tWNjVCQfn^w)2%4x;SEs%0GcZy4lI)4HoU^kIC{MmcEex@2v+G zyRhIL6B4#`CjbOlpWz zk)IM07A7|mG#nhcVSLX!mhSHE?V~kek_}@7y4GR)s!yM;<+UTbVrFKBtm%-EK!(81@m9w{FL8DLh-eHt|Cl_Ix&(rI>~OB*gn?JsX@|>har_lxnTR_%SU+9#a9UgnCnP0(#v;D z>5Q@%ggGqT7+s7a92#|{d7M0bLFDA)P5;pU%dw5n$PvGknk+}vEv0%ts*8uDVb(l-X( zMeT>0iDwjbvaFO^i@$$)cSq|%a>#=ue|=+WQ=-jCPG6?>`)r%;*Kgh&tf;JPsJ5h} zD`0%CTW-OgJ3BtwS){b@@K*sC>_frBhY#Nn2tE)7#bp-;RLW~#o$0>ItnBr_^>n?t z>B?!&6NA~~^@^?IzO7l-0ja5}Ta$YFr#ve7C6kPOCM|RArn>7VdDHYL}puFiYomRL@(Cwxscy<8V=_sYlQI zv$L$H+dA&H8|K8$Yt~xmHI#?V*dMx0J@&hrOPI?@4h>8$&d=1jtm8C>5Kdt(H2+-W zu{8ZuZsuuofs2FB)V5_EZ=${;}>0a7+Mb>e_h$TP7_qtb`Z zC2dW*a(Z_OYCpT3fBoZ=gMxxpvx}Bj)YW&P6@s%dmFfG)7dezXr=g}+s$*TSbE@3| zYxD*;In*NJu{fXo+xs%o(6;L*u z)fqtxDXY1tW1jftjaM+%T~a+!J20i=$&)A9U+=RS%}>*`lGvD1Y`V)db8g=t>gF6Z zE{L3K>obB)G>gnDtFaKv5Z2F^FmBh-ce)YgHrY}j^#Q?`#CRg{Q})7_5^v@-ebf4A z1vLANj103r?SBk2b`@6C)YJ^R5Sq-bT&pcIj}Cd)H=Ifz)GXFuV%oB03vI4*sj$?A ze%UxjJ~uX&7EE=pjrG;k>?wTcrI~AIb}3S}fI)z&ZMo^1+O=!x(e`$j#c9 zL^6w%-Oa|~N78sa-Uc3?!TM%^AxIb-ftN68&&6}xu7P+j)LRqT87m253jm*Oasqqh z3RAG2y_V1aqIy~HC<)3e@dTWxH~Mh!BP_1_u{`HT$3of|xe0Gwv@khM3-D;`a!?+g zvB>HP7rmbsbXe=jL>Q79IA-IT2pO#!gf2Svt4KQf#WAM?nYa1>XFxdxiG76)BV zkZDMp-1wKK0!Z=fIH;=eX7cZ3?o>uYy!v!P z%KiNXIqRD|b$8dQ{#3S64N!uqP7jZ+oUX2}>~BvLjfR?2Xb=ucLqu_1F~RzS$u3En zDcn8N-h2@ksIuu`pb~%$LjC2yIXN^h@Nkm~X9v?wwZR|i0#XvW_2sSaSf+JnM7B}~ zi)q+nJY5WIsAl@jD_uGD_4UZwckI|v0%6b?P=z;j@x9#qnfWhtau--!CK@%EaRU7O z-abOP2k$d-aY9hJa^=e7<{YrE!x8SY1JA{+^&2$x^z_PJf}U5R*pSlj+w@?TX*D34u>?T$jJ=n`aeszhpn{eER;E*5)o$8@l-|j zg_ydwHY=c@s@aYgnLRowM(4EkkfTGZ6a1cbyQgBsi76Q?=rD^dIf;dUuwCa`JCzye zEL$@LdmJI7@XjDEUUO7r$BQRhq3%^HlrhuMHy z#^^19GQ_ymj2ANq?(>$0e`?@VFuz3O6L8kieTAYOKxMPa-e+0W zQyE+i{s8oO`pcd499~{tmd?6-r?KtpHMv~BpSqZclqS!lCG`diIb6(AyQ>CACh3Vl z{EqFu)Sje0XCXIUd&YZRQtwgx$vHyNEa6!n&aQY9UkSNraiXN!AXMC1Ii3T=akD7% zZnD%N*G3KAxcgr8KlMcsFlw*_@(4!q5kXRCCXOQ)SGrRp+D{l;c;e8ZL)-g^;3+JL z+1V1xG83PPqjTUR4)|kjt3+A)HnU3{*>za+(S)`=_5H0JtcpR06=v|gFHAEXJ4KAkqW#GZ>uKD&rR%n^11O~*N03G zx6(qlxvtNuDt@3F=|YzwZu&Tl^~U%%LQorQBs4VT%nlM-)BSX>df`4k;Y}oo7#816 zPYD#zD65ZA4DCH2Q|Tw(u}5yUE})B;R{apvF(!;n#OUmarTH;4s{bC5*8@`o zIINR-m{c%Hd4BZ9MeSj-uC0=GwxFDxT#p3*qt9&-GA>s$TTd)bck6_Y0T?~tuL2>o zF()yeKu{dg52{jsp|CEow{`isn1}b8(v*aDb zi$=iPZ761OrSWkQtoQESGkZnsRhC;AFEJoj1PX*^HqKGVg}aa~ z(Ynjap*GSwA#)kFD1V|!Ck4cqy)fC@dZOq`y=h)XwN*>SXGyEN*XM?{lV$Gg7t}oQ_p4HoSy{+9k`#*?SFz?w=Wd{ldm3; zbqh$MaADY*Z(|;-AZTlkglc)>ewZ+gfL%y<|L+Bb$l*&mf++je!~KdYF@B?MImgQU z*vKRJ*6jcquk0g2#(uy*fI~X79cSw<^=7aTRv(6XkaP0Q;B`Zao zKwxiYX=$leVxA+dEBkvfLpS&*NX`;`$pETK#EWLCI2$&YgD&Ip8K+ybN&*1><>Y0rJBPe z`6fTUeYEKdcgQp& zl(bqG$rR31Ss&O~GdMViV1aU5Q%V&6TJ{k$moqTAy)G|ubu+{t|sFPU&`*qApsre>?n6A^`ODtJ<{QrId zso3sdeP=TZ2wj<0?X0*aqM40L@h{NS4Q8j@())cNE>d|jcIWQh<&7Fxj>v@}Qywr= z*w@g%t|9*bF*{>=vx4?uapNvXU#@mJnwgnJwih$Q{3+ZGMRI(Wip?Pw?=}2d86d?z zve<)Nyx+qdF}dWAD_j8EzB255DYgVb0f(YF zy)w&xMvImfks*|6^Z1`AMf9r2JHjWdrVxe|C8r7pom5O4gWy~R{ByL7r(yg-5omP+Lx!a5>>Ud zS|HOpTLKAs+CPTvpu}2a8%Qi=Th?Fs=PHT+@L^xbtI8qH8NI*11tM!mrZP-= zs5MKNn>*8@bA%$#)FsD97 zPSgo@v;=q6<~}#fU*tX(+Id|!E;;!~x4kq5tI^ha#A2Cz2$la~WG1hcQaP~SzaoJnlv;tXqpL#MkT2xdzia}tt`mn$` zB_*ZccET00cL@*?pPz8Loi|Ersc{#xIT5<81~$V8Jh%;1USjc87iV<>Yh>pvd~ha% zVvX7AvBKP0R_zp{@H=#j%50T8e5JIZbXsPslWFns&|YyAqrsxtM&Hy-p4+YWObl@5 zE@XoE5#s=vT@dlTtwAufYU$DI_@8(zRyjOGqR_Ulx;e+zR2_FI#Z%2TB1)IDU#8j!rzp!@WybKkUcYP4@sW zf~)~fP#l!k;y*M!g|*c-__B!$bx*p6idj~e`@K5zpse^FLll&1xVa&9N!_}vv!WYG z2b9D1FzgocJ#}Z}4bdg2fq&9$g`!R>DTluDt zJcdyaG*MNdLR3J521=!54PxH1fh$pztO54%e}&?A{O@ zH+R=~gGvB`ViY#={A5-U2lIhTUjP34?=JFTfOiAF`1f1H_ZJ4B7fw*D=3&#JInQt!6sBC!L(>*-!d%2Tr_<^@(+sV zsjqt%NE$3`y!zH0to(|mrsJn&{Ubu^iV91Pg&DP1N**U}E1F2O7QGjPz(VKZWY)3! zgdLOX-?#rm>8w6k}#`&T(x9hm?NklM0~N~HUuWI3xgWAm(?awZKZ@ZKR++g z7iOEA9YP}WW$xX900bsg6b#4Sp<%FJiZYh9Mfwc177)CpwM8zpMBOsd0Jpdjdw z*SibCs9;vxlgWioC`iA0z-jKF1XmsfGQV-{O=vf!KzWcDQ4}4$-#B|HF{|SuyEWA`PY~@M$lP-4<94vJ6uW`? ztpm~My+1N7Ab#d$q}G&#jkNJ8(xqft>JYI-va~iRq|Tk4B6xYZLyGmlR`0B7<@mbg z4e*TU17P3-XxOgV8CeMI?tusb)Clp^rqZYY*^o@U4uBGbBdd>0SHUI?SDU(6h09ZoV>4}(ol_)uVTBiIAqs$IK z$V%l<-%#ujyT-kS2b=mHPphqTi?-;!V3Ctu6lB~e{Qn0MhNDW-U#WG zG0e@oFISPCknz0{>ax;_I9G?yaqBg~3n{YiUN*d!S&$C;ehbnehY)Eyrq3Jhj1rJ zI}8rS;4g>NPr(QJqziN95Q3!AGmS;;`fGKAVMxqL#$+gdqT-dmnIBa1O17Z}L9J8j zCPP%YHK~cZ4%hK+xeb2w4DC6Y-dI_P(|+n6kI?~tGe}G z&&E4H7jo+`*zW86{1c1cAIL!uCk`sBd!+2%)S?8=&KN>1G!udvSdnm{-dAn5rQI8& z&UoPWL7LjBA;|lYzjhogn93b5gd(9IlnVx&8!tuD8*oV6+DqoOduaIneoyX7+Z}WJ zI>ccZxM|=NF~zvNY^Wtol%e9K%HO|P6ECwU5DqLT>HJ8KpqasuY69&f6t4kCvOYE{ps2-woG#^b$fiv zMImANStnIuLZY> zKq)XhwO{P-WGO9ZT|nA)4p>{3f3M6<>Vm$${$|b-5)fwb<=k;_-6=OWw`&?2^3WX- z-0=H4EwKF^yLJuwo>Qhbz`qO$33-!{@GwAj7Mjb@()L-meq;2InR9w?`O56u^&t7B z*ub@Inow#gvl46F*dW=Y3DkR%{^$P#WAUHNsFr&vN#QUD+lSd{sg{XIyz z{h4^qf3T?i`gbzr<*Qfwwz5lXo0}T7Zpbe#F5V+%@m%@$BRQgBmgsu`c3L$9okbp- z*~AVbkG24JlnnMO{+*n9O;3+wZgRL%N@jp-OeGstfYHJ@ghJj)nJai~k za39BMi~s{%>;oHq9iR9GMFR9WNye#cLs449sKiT|F?>NRD(ANQ3h0ut)GG|HXx4@@CqVp|uVZIT(10rzOGi>-hjq4#3cB zW}6cPMQga+iWUM>ev_Cu{Q6-(|Lipk0hC#G{`>n~N&L`VS{?rmz#pAe;iH<=deLMwSPTZF7@PVl+?# zHwI3qYz%6%MxX%H)fc~6BDxA9zZ{FO77RY!Sxa~-pUAsK-ggfZ5~SwuZI@91&*!^G zgu8nt$lfH%y1#K95!q!M&KByiB2efRBK@YYMwozgf~R`qNJ4UfiI?uI3m>Oo)Ir_L z7#%|7meE44 znoITsO&139(c0;prO)!#eBexbz1(JW0s1^rP*n1;dpDT3@msEEb!|OeZjAk{pI@Ob zks6kGG11=9aYcvYEcSfXU|qj3n8hSKFV@xW7GLCVU*Yu`Ypd*&{UyEabL$IdJq#gc zL<6})C>yO=Xvb?2kE^PxPwh$Kbje#?x6@RF&l8;&E_^@4rtpgB#dC@{V|vc@hI&b| z3f>|-r*;%@1WLN)LA3)A+NHJi*F|mkjW#6?Ko36!OrgXUaYe3wcqBx6GnW6o)k~Vp zzDXZ{ABLNsQuM!@>YSjz`>pfR!W62pSmwWcxfhf_{LZRkTH23xgg0*xKwd$1<{SI9 z&nf!TmL;ibEe?57>aaFVsUp*jDb8xk$_JSBg`@HmB3~_kq%JDzB}2bsHxjyq?5YUaHPncv?=VMmJahIpi++bgoHyj@cfg@^{WAbz+oJ*#8 za~Oc$H9ue4My^!{-4$Kdl@$o?*Q}fW7@rVq|0yWww{8Cb-qJ% z*$8{_py)ax1o#pfggPP8?+F)jtBUce2F2v{Z~96&-!5~vQQ$9404?5@t>8OQSBGxr z<8MG3dFZ|{E4omT&S(zasRF++JK>`gDu%bounSTY%Guvz>H-WdhyGd9NH_b6S=3i} zKtcpr^pUT8rZO3%yzhmLvdF) z|NH3*ZD@8lL5U#wjCs}hu}IZeOP;0aa=BPPjuVc_s^(JaP(Sw8z`A7&WJ5fpMJ|nb zR68$*S{Jea&CFU#7Ptk*P{P-$as%D!1~vpvsv2<3GF#21uYxZ!y6lSd?9we+Xetw% zIw9R2qGS*T9HE3IVpbo$HSyP55IhTCK_eo?=`uJTV7h#J`8vq0`(4D&Li6!57kTrg zkHH}71bvRT73t+BDu`^B4LxzyGGGugb(aTNk7*|rEjm-QmqwQsil#rA%GzEMox}s{ zz)#p?rjp+nC0JolMbWCl<~!P;a-C8+l#qOY7$-`|5l|<3#V7{H`ogL&ld}$^&QS_% zM{IWzR1Ihzi#1Ch4bS})HD>%8GHmGh#`rmuW114kJR`bKsfQ?|)uK#bC(k|9NdhyB z1x5_T(HM2`f_YNsPU(DlF2Z9u6sV#9(cB6E&qW}?bOEKgX0}7PaN4MX7A9$XH;M8E zy5NeEBb`MW^ESV{>mI19ixUmhpG)p!!M#Tj9qk12wl-)tWd_43_Oz>F$o1AZ1kG#}4{`W5} z-7Ehj^-#N@XJh-nioPCYFKQfgplVs4MfgCjd*_JqS?lz)Q~wSk@NW% zLFSWK?KV~=-ca_?$0sdUSpK*QC&O~30fcqy@X;G;M{nrlQW%P4QWp%Y!QF8>#g4oz z2j|Bj^=x@$NuVgG`XjbB0+IL*R5+9hUr86~hORg2Pd%N|UF9r@s&9#Fz@`YzE0>n=pJleFKW5uiY-&A-J7;T}bF{eNc zwZT76`t(rK<_x(#Ngz`2DQBd_oA`JIHs-`oR@Gj**zmOh} zDCcm{{khvnIely+9J{=qb@QvnMzbgB<%xB-wdXm+;cG&Gok76cf6H?k&LA7T`*(v< zkyTDlke63;x|yo$R9>eaP$q>D07DalNt$Gs;M}43R|-{WR_K@cA^!}0MP!(?d%~{! z+<}r*HtMl7CzdE~@YZuzz%QioGV#e`tL@dy7y;z1C#ZV1Ng$Q!nJr9E{G-QBP~{^; zf}L-4ouniBr@yIx+EBN7bnNRG#DacXXltaZvj~w8~E* zq5{JdV3Eg_W@tCm)^0;SXw+@g#jFohbSJ8WodA?coAOwiLNzlYdFR_-vFl|NzWh@~ zjn2rv218cp>ywt2c>eYo8J>4pnlJUT)~%nJ`WUT|`LKQ+!5EEk1sIfj3NNPKtb0A3 zv@}s3)ba%B#jHLxz1oEYCuk09;EM+znp!AK&BbS3sh52LP!fm; z4!~4Va{bDHy}-tG%;b`;%W#GbC|a48X?URhBJ|KBL#6of_aN`c9!jk=f_Q1x2S&|| z`cGXHT#_H8M(JaWveO3I-{T9a!RM!^^qz`8k6I|I{zzz4FRDC8i?2u z)JAlKb#*~EMUmnAd{yVX67nf}SE-CZ(L=UWp$U9WC`1d;7UxFG*E62jRy18IY=rn% zpN`c)Wx%ikIv6BBIQhV3%K;7tm2_i$MMv!#-^?rsXgm*iWhl9rzFlY>v|9)M8cbl0=Kxu&eborq+!OyeH}lBMzq z;NL2hBOgo=Et+c|MP5Ov9HC~!N}TnF{zX7368N(eXo2%sRw3VId01n#H>I4@2fE}( z2{;|wR?DmyzXg~JPS>Fw&u<(#`#Em8S0RdPD)D7GRxg|P;`pdz%=(pLEQjG+&vj4~ zh@>9nerF))D<5}N`5QmiM0K*~)pTO!xE_8voY!dTBHpign+ zIgBbhMfA2Op|(BB|Fi{j9K}1A7N?{SyJncyL`-)s&3VN6LMKmH&=sE!=Ml?M!xqjA zv`5BPt=Guy`{lE1Tl4dhY?~}`VBn_rlclIyX?5LMd!vmDkP1OUdek1YAX93z61#Z2 z!UnW9E~|EKrp6=Uwkx=w5@?3%ggZ2M74X?-P^iV zAK$3PUtX1WQ?#?`NW+-@L>}Vl6fLwCSx)3y@Zv2h5Bx0#Nb@ik<`k76dO1)G{#FK)Cp(-!+6!b)PA`mlZ~-yKzM@%4 zy!}~Y8*20b16H+6%(}z%lju$$XG1eU6@f4E_#C7Gj==1{T8(x5dXK+6SogL<1+7kp zl~dMLBJoZW97T&%mlx_#I(6z4I#EJ!x!w(}PbEoC@N*PMx2mM|TUK7t3ot(1-hk5b z?p3t$7BDK`ZR+xJIGyoaL3$UL-Jxm(_0qeWm?KeVg_F&w=npH+VkOOock9TEz^qk;MBWvsRSXHf&|*nfa85YCSbd9?LlB zNd5Blo|FB`OA9$Xd8hH$KojJx<)2q>)okzMQ&vX}k!9?jkMv7RFAEac^c2K(vn<4x zJjSS-&OlwIp$we*(}{WLx5WAtz!MP}ltWbynqd6p2=p%lFa&#me1EkXDW>Hjltqv` z2|&S%n;R;Wh(>|Vd87|-E{7O_+L=00d4-{wpb-}n1~f$(af82mLm#E2z>i0r(ahoY zJohkqL}7RWsO|V>?W$p@y;)j7NlL3m_|Tz?Y^~sTMIQK8%S^SUr$Fbk1aQ0&z)b$` zsw_5(anR;Me$Smuv^LwCQXoq-NZr%}6zx5hN6j;C}BrgT{>#-@Dm=8o-PLOZHK|lU-)N4N*ZDqQL$>!aL-TSLYNu#lX zsQ5WBWxc4idj)J@C{g4M$#8=3w%+N0ru@n}#4kq0NpjNCWapwzT%*%qd|a#_wDtt3 zGYVvc5X-NrMxi4JJ&q8HQ@~=MrOb@=nTE*$Pve>3`by~P!Vx+zgfV(@k>=RAJGhIt zm@jU&jF;2N-1{pRd3g+hEi&(M7KhU$fqtnR*Czd}4Tg{tlN9^w5VQaGd}!zV0r|l6 zA&6tjY^^}c0w)&v(Hx>GBf9yogLU>;aUnSJ0k| z7#l&Ui{ol5#EPr_S(dH4psII)B@~>r4^H>?M_&GQM0mP%&pZ&N*g=kpVXfhvN}0-% zJ8E7BDR$ z2JRx?>xpvlObi zaDIftp^a&&r)WMHk|)5;-C)FiP||nM4rgLgjlJXz@s*9=7#*uaXR`qL)RUHglN<%y zjbTX=;7i1Oeb6KZB^2L2p`33|h)vmD4+XZD1KF!V%0iTkcGjw3qUl*Tcqoavp)_Og zOJ?LnI9BBg(=YOAwaOEuPO!1Ec0t;RDimc9Yg-c+8MpmhlxGCN$vy^(dGdkKUJ6{7 zlZP+w@061{qYfRhU7xsJP;%!=)#-f51yvm-V{qEbdDime02GDUl(Ew~ZHa7v;as0QT0c?cG%qQ+&u*#Nn_AyGAF6%ZB< zNUN3-SU#5^VtgLa3{Y7IXnoch((1NZZMPNYYnEo@LjA3C2V%2fkEMz3zjk#t0x-62 z@T1;;8@SykT6Hz*a)dtE)Doe@@C2&E-*WGAS_6{~6>|;>2q>~(;lveDzaY0X2O>Oe z6zr20ui}<)OwrfN3u>+9z=1*g$9r=e4u6+V6;2hX_sRYEz#{_Gu&qhB@%}mlmY_{9 zUokC8)_pp`il`eqcJ4d}0H$Jk!*1P2jcAROE?|)Q0#EjspzZ`jv8?9Df^3L#CaDih zq4{roa^5f{j)vS{>loK-x0#BWF`Zb%G|i{oc3I*q+F_g?@R#jn85)z1WV-2~}*Cxm#)~F5z&- zp1&Mw6)1X(B|hHKECt28*7WnH`JugXmE8eyvQ+1grNcUNy08nS@V)_K#J2}FO36=N zA*5ZX4x%_0W}>}h4UR4fN;KU5^A^A(4jSh9CI6GJqqk}*v;>ik52-R~^kkp8W|})l zd{YOgQ!6}xL3o5gg&sPa0 zdc3OYF?mB@#QM^`2-Ow%og94vX}60e45CJ8%(6h^J+;b#$-Q`0Jx1C>#xXszbDK!I zOWXk5U@gpfNyW`Z~nrh1LR=OPqb7C3%Q!;M{i*n?cnuI-Q7EOzxc@f{7 z>6i_|!@_ju<}Jipf9zio54(?&Y~3*~Cq`LFKf8ftVq4Aj&n zoC|P~MY*|@H4n^HQo2~w+4*C?T&HOfVK!JLfwS#SC^MH`MAeU$QG1z>!|c~QGX_|T zXI%BiW?U;9R;GIEjn&|l$*;5TsPmKW6Oxv42F1BXtnL>rG^Vk*=e=jqZ#=a@Gf^Ga zO|uLR4%UfSDLi?e$%sE^memTc*vULmMQwl?kEtXqE-nU`4kZM6m#Q0cpZ{;c1_AxUvi|Qs|ghjPn+p zP7op$xF^@${EQAo-0>di_~wzYIL)ON38Jk`a_?|)8-JH3uyJWbW6punh~IkjZEk*k zo8zZeP{xjqH1s+W^m%Iq?-p@}fR%x}^X}!wJMV({D1O3)&Xd#5^Bw_93A2?ROO7>p z9;y=^t!ks;vM%$pcNSy^%%9%boZLjkyDdzR$r1mecM(Bq+Wzj^&rva$p|lBSJs_Sd z2?ke265%})&d^&)sJl62aMXTauN>`s)RhJIB}zum?Hroj%KLLL&DW2<&}GAd8qB%T z8XrqFt1QF{Ph3A54-e?^UKzVtgV1Q+{Yp-pV^_&n>1GTyR(R66fK+!geeE^k83&Y$ zq^#%`J~D2+1Fy~@ldAg0LM|Q9iTK@7cC&SI(i)qYr@(7wwC=n!sART@WViE#$3s}R z)@2JQ&(J)aDY3GPn7QjInil!a$qD8X>{d3cBT;u4li?LRc&TtdY5MZG~ z6SyxaN&g!%!}gXX}xo7Wl!#>-^h zfBjyjxxzN|To4Uc#{NI;U29C6RT%z8q8P2RusA1zjai0Dd8^Tw`e5}yWZE~qM_n@cYiN{#)fL4%=GsHgN(xuVpX6N;b&N|ZD%hc!SGd7MRP-GJH9rBNCQyF-$I0>SLXXdI`2VAU zeCegh$TVN8oly-#qUa!#tf%+bl+k!X?BBV6+vo#@JMuSt9mKgeEX8ZQyu1*&mBv>% z>MHrRa8}7UgP>>9vNHD%?`Z{U*sdCJ6y%2yO0e*|`c`S)^A9`E2+y#Fj;S2moLmi;O|e<>jw)w4+!m zZVME8I7{wsq!<$On)?>T_!o{vk~Bp-Ido+a@|w->@^q*G^s0M(3UEj5y1DP-K0l4w zI$M=J3`GIBFxk^j3Z*#9idX*nG>~nJ#8XF5gUQ5CJ2%qoVc}vJY#5&hUlQGit<9v2 zzKG?hW|qpdW@k>sPGHWubNZc)jg6T)^N2vd-%k^#&wE<@OoK@hTchYD#jLk~nlC}m zp9#T0PATWHn7w%NLUx2GRym3tfOYvb+X)=ORS?hh3|a}LzdtXznvTpvM>_LXZz^6* z^7(Q;x?u65C^f!8v)aG){}Q$l1l^7cW{-8 z727a*SM}Mkg4L%qvRj8O{{JN3lOKA+Ol(Kb6MlDIiO}lCezHqOifY}-nq;@~r z$}l0E)Z`@J%wcQZM1q^vPRYDQWTq4&%M+Ahlwx$`gwx>kMm9QprX=`3b_Z+Y=R-Eu zjyQ`QO#?35=9;iDkQ}dOn9yxxZHb{jd&f2ulT9EfAt)iJLXZg$jRqPGG#Y3$00Alm zR0^mRP${5NKqrzLQ9x&mn_@saMz1JRLi{%&?w8a|r&&e}XVxD}eK9@x{0qVl{szF{ Bs>%QW literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/error_alert_sheet_0.png b/packages/stream_chat_flutter/test/src/goldens/error_alert_sheet_0.png new file mode 100644 index 0000000000000000000000000000000000000000..f484ee2a0c74aca9f277bae4ec33086c718f4bd1 GIT binary patch literal 42263 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 literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/gallery_header_0.png b/packages/stream_chat_flutter/test/src/goldens/gallery_header_0.png new file mode 100644 index 0000000000000000000000000000000000000000..a0b780e9357d01474a9e14a7aedd6e7a214abdab GIT binary patch literal 36757 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 literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/group_avatar_0.png b/packages/stream_chat_flutter/test/src/goldens/group_avatar_0.png new file mode 100644 index 0000000000000000000000000000000000000000..bb14fc09dec90d0e8752cddcd9505dfc543225da GIT binary patch literal 60051 zcmeFa_dnJD|39uxWkuPmgi^Mwj8Ld!6gskw6``zSg@dxUNE{hO8Ry_AWS%3tla)A* zEo2>g9OGEuhk6#hdSAYOz~|EWCCBA)AGhmmU2nJh{h<&o4He4c%*V;d$SBoRmG6*| zQJ9dC9U~_{3jC%7M>-7ra{zirMUgD8g>@46au}+prb7;VyvQwr$;dd#)ReF3xF=2b zSK9WivNbFe8~X96DRufP^4Q-iOnA}hcWflar}`GJ=X`Wku)o3=)=Qr(c4(>ICVWt0 zpbSuOG7vfPBv9aWmqwsWaL4P?yU*^gFMsV{9Upnue|ZqkG5>w*-pgi7-Xp-$KR#?w zF?dJStR0(X<|ED0_qoZ*W#z^Am6LbB>GvM`_0YGgbKdjUwpI#S2|Xl_UhTZ;IxqwW zVVAY>Jj&1c@!)^Yy(&=Fb515Z@!wZp4LZ}d`v))mnnhaE|2mi=|JT=vBeZ9Xxc3H2 z1k#wI-KOq7dSoA#e^k-;*A^8}%Tm=IbDg=QE#B0Q@OMdk~HqQ;c|YeRW~1ScgY98?^idpb=fz#& z?iTMs%2=VRtfyYPE8Og<-@*wUZw${*4By>a{~q2ZVlQY-i)eR+D^uCqT9)DYY8%nr zt+|BlZLLl1Y>vRsd1?0My%`UTrT;O*wY?c0Ts~#$BD_08k>=iSvyA9XU8FFHFIDvB z`bxN}ZK`@6UXii(+;xZ_pTe03BH4cZ{n9BKNCd|M&YG8(mslJ`Jp~!`(l(HhadERp z&8Ve+JNO&%j;Z+k?X9g(zb(Pj>5UH8aQF;7iZQZ4LJZ=UV`S_f-1v_{p=?J%cDW z`vH1WBa=s0UM3gH{&H`!K*DboO(nBP5TYPnw~Ib>uH*!4T}|#x-C54`aCF!Et*hkW zJzeE|nY^VZ@3rx0i#SwcY*bBZCY6no3+~gV*tg~LIk_`4Wd)UxDHfJTt@>Tdqm3Th z-=5skvntt>hf)$oF;zEYDqHDO369V2l#np0mfrIsGG;(t_C0yURQ<&-?dj-20=cc^ zp7>u5{t+Bro&O^$D1`kH4t!ty5eLZFIR6L$&i>>`pM6tvoz3y4{p~CY4Z-;&r9Cm& zxT^{KyxjRYIdWs)VAHcXc6;GTp#gBTy$D1m3#`nuC*v}(vgf|R=f-d7$!~W7ddao7 z>kQIN=_r@5r#f4`XCw-(NfG%}D=UQY?wpx>C(2#7Jho=r(ytj->+fcVM^V4bl1yOk zx6W;CJ(PUGC}0zmsv|cN|gBMSD|Bad7w;7NmOHcy?BI z<{8;;N+~k@&WpYfDNl;2mTm7``-g?Hwx;dzf>Y~$$07`?oeum;-DGQV1@ZYQgO!l> z-x{nJ*rz>Td~p7mJ#~?_oUitf*#mL@#@U>(Jze{r2+1D+?Fn8HM?U4Xr!N!0Rq*?s z4hYHjR^NNtFqC|1XAjhmZ$t98_o+ZxS#NKuZ^!G|_ivM*3F^NWN6GRY=Fq;_Gb6oF z;T!t~n^@PM*+2L#v-bYMFYGS;F*rnc|H>x!TW;w2>`bIfOUc>o;w~XEwbfgUM6^WY z@7w&%vz1%uf0O0vt7cl_gO!Mb}0zq)P|od0zVadS^Wg*naSQx_L> z0s@!Jo{ph_HX9lRX#D?Vs8m8M=QizMMY>r@x!tKREoe$RC5H8~+@9`P{y=LzeD> z`(tpA?jJ&VSnv0@tV{ccZVtcSkH@#?jr+aNSM*Kqkd$nkDt=~QZ(lt=O!cHvwbqf zeN#s78Op!M{~<&6&j|h*!9OD)`)36IjNqRUko_})e@5{Cm=SE(44`XOod%Dv?%cZD22{!~@5T6-yJ{t>Z%I9YUVZ zR{50Ad(7d#6|A9kB z=KIfb08{wqR{vrSfB^9M7Zd;x{{Mr*J6OXQ0Mw7W06#Dz`CFc0u?T6aWzZ9|{WkJ##-U0kVpvlE!S3D$8hhmKNjOM|&ji zO4Q%hHE1=0FgASsT;lF>8rP|`s%llSReZ65_O|Y5J37D&&^AZ1SDGf3i4T3+3rPhl+Hb@G{OA3w68_CPiO%6lWADpn=FrT8-UKsNDOYjn+c!}nk7ebLN$>|=s7YNp zzXh&sc>f^>0*$4F&7(uSEujuN{M7ybmd2_f*!QYaPg)o@3V!m8gR)c+Nfc+^pSd{INFUkA9M`(s+AR)JTD#acG<;{_Jc7 zBXKx6*p5@XUMP0ttYYb*Cm@1BNT@kF&&rZhTbCA(XD}P>!nG8QU&Qo~z!GJNOc2~0 zYIE9N6ocrI)f$Mxu6@Q_M9DgHFvMROc5)H$p5Yg47+$mRnlPfS4H|%51q3FeqLcq| z%Yc=cT2?O7$9>_>rBuV0%vhbXhpj$0r zvMVxl?sY@M0$dxApjPsQ<_9LMmLz>Lr-kcDQCJ`Q)Q2k@t565Jg_f1Vk%b$q_b^+k zW3Ot#y($?g@|Q7*C9Q5}F*b5W8wBDs@}-Jk0|e$KE=TMLS&MM1JU6hTq_TT4$|U6{ ztfelNE~4H`VGQOY-)(Y|CIWeCo;BUId0$c@MDxk{RzfylJ2oM&88ajVT5KDVkyW7Og+w-m&Yzf>W1MH@y++WaEEo;@%OxM#NO`ssNX1RcL8G~Z?<8x(Ik_P8lJG( z#KvmYLgW`<3z_muoh3W9X22H6n$Gt;Eu9CI{SWtR6Rl|@$j zi@#7<5Kd)8>JvzBPFfHcEzSCXU)a25sS&$E=9VbDR7al-nS~1cem^rrQct=JcrL4P1Ed9JL z-V^}wSX1nHNPRdYXDf1k76S;(&n z#8xS`peZ^Kd52IcT?G=g70w&%ci_}MCUC0}?8Y0DEgv*<{Y8|UH0Gjw!&u5nXO|pR zEg0?68jhMqN|OJ8WXEekW}&JznF82|@8h30dZ-H9kuMjZ)TyET4UmXnYsaSNsu{s@ z(ygbTfQ+&VM;@fV_z*OCSVY&VFHpRE0qWq=_m!>q2}qaKcKAis@aMQXJvotqB2`O1 z_nwF~#H4RVj-yCfB0HpL=>v*T4_?7t>j960jU!jR$0j9VkI(I*Kqc~{N9|xDyEyH! zNk|v0eKs;h>v-ZBhzTe5M1zI!Q1+p$vFFU1vJli${unqa{er<~y$3?k)=^@EH+52V zHG3m_88c2=_Fyt1%s(q+cq_#VUY=ItF6dp*-SW1R!dx7ilf#mcdyt z=W{%-!c|mYTLcJ}jROFui(gYJ2JUXRantL1uw9sNpg*+};Om#UT$BgjQr zs)62Nx)k6VSn`IX4CXzG>MKT{`B81d&Z-RU!l8u%`I1w+>mK`gLtD~;-D_Fj%bRF; zX@(vWN6|v{475I@cw5(TX>3@5raj6ua3o>H-ZKYZ-b7{LF=7{_O{_1b{X{?KXuaN( zazmv2G{nXGJ<7-4bDUcKMXHItU>6u&Dih0^1uGKk36}RH$t)}f92*9E6H<29l~nzi zX`d{rKs+vET$T(kT`oS*(4lI1b6R+v@@9X+o6z950i!dff-kzg)!q79g!rT3r)!)u zb3I1P^1M0QXC8eHkr{D9@S`r*FFsp%9gON}%|pi$H!!(t2pjpq#grcKQUU744VDG_ z_|&Ld5O+@RaarEoJAL0?>2>`iOC9W+Jwl%0s+* zHFa@II{l80mkZgLK1?;$H!`Hsu>K><%6lS<}&yPI(4MxVdk0Cu|(oKC&Ta_ zary?tEViyB0ZCw+)VNpCIvXIZeR{3{z0&#QZJh0~voWP~3%X1+x-$X1_}+Cdw_sGL zfeEB}9K(FszLqxyCU$j``XAM2O zIH4?}&Z;f15n_=B^XlC?@rdlbtP4|<*idV@LPn&i-PHIu0^u$BZiF^!v=9P9qzgxT zco=y$0#V}Ny$F3dzJ@?^pc_a~Mi0Bsi5aQmrs_r7%-S1+tC)E;3fJ)*&>9p(81v=ORt zvOTiVJ(H!UV-9(dO6+yZ%DaeW;voiXMcx`B(F;~O#x{atn|5*=Z#Oidl4Zhmm-z-+_{ z^ZngC&|G4J2OkwB25EWo-`+|odjgWpGk=k5&fk#t!B$4$8AuvK7;Jb_^dribn?ca7 zt#xa)_`Kbt!;juSUzhNl-Q|_*I4D}x@`lB29>MDg?_BKU*RoneLMGBVTyh{X@69>2 zi4A^FK!sBm%o0SRvu`CJ^b91Tvz1=jJrG-lI`lg(@d_B}1=qceIK+Z|GmMlz+B4)W zF&@8BhY2mWhHPyRZY^|mNBof3_NTMv_gcHbK|YX(8!s1*4NCrC7oI<_U{7ZclA;TNd0>WmT^|%E=B!kL=a7`vsL#UnI<`5hqYGjWuDVqr0s_k2lUbBaIh$|Kk0F$%x){o)*MsN9gO}x1&XbcxUS8 z3z}}1(WTM{zIM8gcEyzjTO_)iY!UZtPA+m8i<*V5=(-su;f)%HdclK+@*Gxoqv{Q! zHheU9LkW7QQgL^E&#`n!MLx0Juiz}ait`*`(+RZqPu2>fRWo+FYB_c3X>aO=PYJyV z)=G5TxU5W+INgs;^+Tp@Wu&>z4x{ht8eXkrypqWWgc4ZA7ya}8=t`|p6soR11c%kL+*luy%wWJiN6po7h@OLAIPeJ@n{L^6mEn5F z9dQ+&gYb#)sT0Ljpn{CE)7E})yORa&tyMV9=Z+w@PaOrQyn!U7>e$^8BoB>^To_v# z-^OPQ;%r_@3F{82kGLyZ*NLA-rMbD4eIk|%RBNIj%GfhaLvS7o6^RKMcI2DRg7nu5 zH$N-$fHQyUa0D@ZDij7W5uNLai8587rA$WrT;)lNL}%W_Q61ff4|7_9h{eC4yAJQW z>2)8{4JI1;tS|qJ6#uP-mt9`d2S_3VZDZ%4bfG&8&}NiUt2(vJi+HPJyNsw_2w-bR zt|Ax#a^|_tlWqMJ9yZQkW?d*8|EwuD zr-;Rcxmpf|z-Bu}e*pXFC$QjMVS)N9EI4^2PJ>(Y+?UBF-4w9Dbd4iu zOYbPlBMMR0->}L@hde;J*aFP#nCNwjj_@B`3}G2!05vKrl)^u2inPs@k9RVuK6p6Knxq!W+b{=u_A`XqqBJ>Q_)A=0n zIi-2h-IC?#RS^@zkL(>E8II=NKBAi*7?Z^v3ySF$>TORAmTY@W^KR7xTyRdKfa+z% z4#%tT)8+Rz`ll++4YvWWwK~L-|0wv#d|8M-UXFa(7r4*uSeuK#R1_hy9j_L4)Z#sf zee;A;YI54bH*^7IO^tn9KC$WM*MPG3-8YZ(_FQgwmQom zQp*fF)*hy6Ib+y1D0cE*8+ci_#M?1)z*V4Eusd;rpJW5e-KnvU+U?c=3u^zyS9CyH z+g#OB-TnUhPz^5w6}AKaGFojhiLdW<)kVHx`@T)61I~>jr+W?RfINW$z7gHV$79+j zv6l={_KvCI0lOOOnwDDpo5msZpcGTmGi;vBK4 z1P{fI-n!hk7D0jKhOXb$qbpmqRfM#G)uBYvi7m|?SAicZ`@U58@7hdX@XqR}8qKuo z)642yOdPz{HSR!iGIGIF%r+&$0P9niG0x8BA%$W>IR+489_Ky78J}D* zOqOZPuNhSoxmkVMR!PKGk+aIsd&OcKjgGWjd3?MT#m)wrcuFTgVrN~LBWPYsJb%35 zaOS3ARfAKEm1c5>M}eY9X(IBm->&8^lKRBJb2HCUQZ6m`_s`q^G*Abl!#`iPQgGZ^ zI+iXzl)%SrZhi@_6+rMDjt+0KlIy$O%I@#}V%)eQX~NCNJ$tK08|~tGYi{R_HeT0p zNS@`O$hE6HS>j2Z*9V)?jtV?Yy*ja-gPIyZWwvD*U@@HN_c*=iHEIHRqO}OI?z6E~ zy1lVe_M_|~^NsmsLBrlq_l%ZgLsgyu_ybs$nM<}8=#A7SZPR^Jl+D^`<@8iH(#nQt zoQr_ns#%A^7Qd-@?C`STG{X*wFp$@*u{P=+5*b=Gl;m;OvfaE9Xz^7)otOcv%D;l! ziiN8E67$}#9~{Gy-H%-g7)l@%2Y*$0XtW(K z$UEv8

F^=7GZ%=E(Qhe=;y%RBMX{_+tA}O;%gs|>A zi_jFySY#pD1=AIu3LtbtkMP_8dFpF_2PNOZ`!427g8k&JcOSJGezFvakIM5=g0)4@ zA(YhUY-@}R`_;Y0A!2ed9c<5;ZoUK2JIGxn=>B!|wK za$P2t9C$j0H8iQHJ$gEa{;4oo@D*gXUjhY?Ay59y5}$CNnFAsPt|fg`)3;Fm(Ag-PrGgr>4rLpsk6@RRWhw{ zJMYIzwQ@HWS`^|lG(>u^ZEYa=D0-Cne6tzpW0{RPcP@&R`JRRvfQYcusEaqf3O?{Yjb%TqZ~gEfD}+aSUlw7g$d)j>^UQQ@hCz~jUeY+rfl z$1y>e4|i)IGc{T+Nwlp=0ddx{*KIhESzHka^9UK>EeTpZRE0<0@$UW)`fc2CDu0b- zs8N^k#Bxu#%E{p;j?Z6Gl!Xnz?#CJxWszj=m9Yc|hj`iGFa6~&diq7}Q<5Z*g5Poh z}>Cb5U4VsWlq@0k9m+yE|7IieKMaU{2$o%1OU9XzqteyAw6or^Q6-|u!# zCtB8-5p>J5-(QJZ?<&{sUlP7NWOahhZnp019J}ML0I&!@;>4^T#*48A>nB#jc$W~r zEN-}H@8?WAn>Zhgt+`l*O4qoW6Zsub*Y+KYJk!UHs1WtefZD{+z>S#=?V}sobk$%^-9G^czS{hgTSEb#-3RDYx`>h zG8GO*vMMRE;rTQmDYZ8?Pss@!pTp=241qP`wYw4un?ws^#a4K;ke;ieMQk5~Q4T`R zM&_eFSf#d*E8=4S2|`5{&!pPYUXlsqtO#-zjJ&JZSQO_l1*`9$IB^S0lCNE{8`c@J zNC(xFrhD7pxlhaLaQf@;D!794T zb$wohuDA!rmPIEKQ*$&-{~v`hZlq>>oLi~Z#I|f%m0^o$P-aw0_WldzAmn*)Q{Nb7 zMw<|>8LjQaC-6B3A%U+aSNz#)}pP_sq{W$025|cIO)6 z*i|d6k|bbFIf(g-y~7j(uy7$amE`%x5lvVh6$#M5KWmF5XK($KlSEAYb`F5M|d~$KDZ$@?voi!&n=tB{JZ%=ynt2_?t7frRnvPPDNpAZgQ15`RX6!g|ekP zRk$>ZkTK5ll*pXD@GNqaQE3Owa$uYYA^SxRe}NSRdw!`vsM!i~n%9{dyde_3pS_Ly z#G@{pBR^MwlmT_^hXNPuP`6#3e@Gw_BF8y!6v@hs@ zWnWmsN1(9ufTZJaI}NLob#<>a|~}ovg4tZ6>auw>WY=ZeVXusDU#p>z}OKr`Q)O z3Kb`2K1Ww(7QbvPe$ie$v;6KWYd0=&5AK1?QhwcIVDb7hxrXEupJdPgp_C!Ht6jG+ zrxsT8Fuq*=0S(szl`S|vX>eJ@e6iNz5B$I^roWithz6nWSZ9nX=-r&X@H|mTaQ@nr zM-OnshbNVd%*Eug(XT3B(W9{5$)K9`ZR3oMtHhpnCkkYxq#bfZCVn=$QV&BS?`zit z+;CEDv3Xy@x`3=ustFCs54q*G4IIFB)W7LBIOyjyyxfn^ePce05XK2h#THq>9#X{p zp(?^FEgxX@FMU;I2-Q+}i>qpiXhqz)-jZO?V;^{19fK|3R7!j$dR%Qn@Ez$c06PvK zswhpE&t_x#Kjp1tWrv20zF~33-dlxE_2O62H5dChu)uBLwF+ZV%=95du$M=PZ(L43 zs4pW@0WAU!vH>d+){Lvkwin7nPsk9wIt$HR`QCd%=ie-h-5yC8zpe;Urh=Iqzec>}1A6coR%51sk(PR3SeJN3dl)r)+ zxG2wv5v+P7zDo7G zJ|RWMHBsw~Kvg;?Mv9dZm$4F#lho@&RDy$!d~WWx3mvi-ZnSD!U~UV63u%cK1%E6s zG^?f8Iv^jDzev1WuULP;s@wJ%Ne z=_~W=?@$QP{3eS|FZNd)l)jXcSo~=)&j!a$=c!pvRQF9XS2yu z`qslctRwfL&+bqf)=&X8EV>rzLn05M4?uTQ_Kr;LtoMFS-j#FIlG}Xk-UE~X?t#uq zHoAQxujdz=l-4u55v^vN8A+ZgGFqS!-2Vboo=CkjqEOo|MFWGfENilsuKlyXsS!i> z8&Da2<7S|*gz)HaDI>>UPGgt1uk<~g4*`<= zOQhMZDinI0^`ioV)2B~s^r!tvPDtC{VLjBY_{w7Ttoe{}C@7}Fcw%Q>Ph4lU|A~{l zpwrwO6x2NU{5h=FFEab6OUC2rQFU%_BD6AU1$d7} z^B6m$0@6D{qa(1h$NcBe-vO-r4k=>$6RQbON;xLlE)d)_a6 zdzn^3Ub=I3TPzmvRI5Z0U~r3}<{ZQduAZ1owKg_TK+u<`tm5en`Rx+*qDcm`GZ-;* zc$u?DnB2*lFXxYYFu}QDgqcr?oJM?F1_!;QZPsV4e5GCyt9krNorHr~rxwc9RWaWG z=c06r_g73Ctg!nPaso*OdbR67k1zkq+sYfWs2o{Cv9RzFqQWjo^S_ zaEn6hvDp?^1o3H1_)pMvXnMo;r5rkv;>g;?ya{f!T zFCL`*cfc#`qc(AmJyVoK3l~2=y8{-^opL$QQ7UIKOT$d9Q4jRb|2dRS9Zz9%x_w4z zLsPCMqwO<8cB#>RFKa}YX12vOrTg!})4Y7DWVqe5(AN%tm+bqCooj86oXp_(EiHv) z7d)J)V(O^*)D38I5fi{|oCeh${_>`cl9>%RGhm5;l-{P zrK&)~v~`!>F9dRi&OaN|Hbdq(X#rYrNOp%@Zo4YuC7^~`D2h8(A3T1^`t|%kh}DK4 zAGBV7iGin3=-3o3!wvSS@>P>W{W)t@P}-PTxuT#ISqMHGg_&M?#h9)T3HP50UVg8| zj5~IvF7_hp9y1)i{7MBvE#@xEfyDL}MCj`6cdV&}7*0T|1KsRZa1ZM?M_Knv{4sCl zL&3Gxt_7l7X|`T5rds0ksF+diO~46kO@Vl1Bs*$&Dd^NNWIrCA9lRWuxW=P2UsZ2iD)qCiph|4sTuMc`MsL4N$V#Sl&RV(GgA~;0I2kFO&oJiWde|plpOCyYiBU=^O%^qt z%Au0eM9kxGig6%gbBuUfm94J-jWDg%wgXx5c}S1RYw8;1(AY-rc(zr@f)j|jc(+3| zzI-kF=7>w_mz(Po!t45-=XqVZFq9`#t)%Pc6jX%pbld&;3#|SrxF9)-v2)#XtWrzG6R8Of+`zcPTV?h2-KqGckvx|(^CTzs1&nKcWj9xcD{LiMU6! zz`)jC-`?6&((~_4Nmhy3{RmPVcc1zm1i?EeI_Ile4J$gcfUZWJf)vuGxl z2c^Wba+0ZkRlE(HPt1>8eZ1Zo>93th<#KF^+99K*9y|>STC_ASF7K&nj>4E@1bzG% zj(Xi_U8aS)z&jvQQadM|N}og6^|}92BbOlIi8-Hu@Z;GK= zdAI0^;jjw@+4*>Rv_w2M*89Gyn43MEtoX`GK1xE`I_2kLonr8Ox?n=xNET@efDvf6{x~C7U^%F>!&sn%ngY^83i+gX8darxT z(qBzSVcH+RA{1MUE^ucONp%SIgeXHp*Zh8ZC4KF1T+X{!Rg0Vb z6m}H4`wB@y+?6epmYJG=#|B6!Sn|P3$*OOhwp`tK8JCo^4<4|JnNP>+Tawuk85hSp z-&-k^b|dS_&;7!H@n!xr`oawB_Pu+NZWu}bFxp}>r*{{`nbpnKAa}=n#INKzu$?je zt&ya=x!~P`;0sE-=XaG@m==qPo>n(c1}oH5U$PEoz7joc%Mo3V#hpfQ7r28KxZ8Wz zteIri7ZB>U4dh-4Y7sH;D1sHq!gHSBqX^{N8CTuU;9aIE-l21;GsQ0nJ;Kjz%=hGK zww83cH9~NO4UiW5_~w-1a_-LkMXZF~aJ$cFy0dd|rI;NUh4 z;l)`%Q}EE#ZBAYBc;xCLuYCf6@^4iyb7EI{Z z9lOW-JUDoQahCc}wpPLx>mN?HJW0HghQSo~!HiCyO+k<06yJLt$2+^bD`Z~l8k17; z7ONOr?WxG}5jlW!z*HM5_tJ?LDJ0AI+iz{ECx*@srFNBz6CD|!v68IiZm_$Da}bQ; z_%~#Yg_s1@j96BXQ#-{)lpQL?fWyz#C5j!8`lbqqsmVqvn6Rd8lzWsmFSF2J9^y~` zxAqLLfBga*tLoW|f!;W180>VZt$D_3vgPqf1gRn}Sltm_D45DzRkXyBdO?4b!YogJ?2Iu|R)#>tMx;nEpsSCs1Fk|+ zVc5+`3ml*+T7w`i!)JY~LRyyMTN=u^5qG-xx$OSC>1G^Cl?yJe=?J0ha)9j##~53-wF2~ zznMkLu~y1yXFrfx8y434qlPDbj=~$S1`ApK@V3iMEseEy!lTjHqyb@EocdJqNRCEv z)$a+q0ji}E*f&H+HdMw&_K{(fNeXBDMuAjlzK@0J%7k%_JBUmY>_}`X1~FL(YkzOV z(dnk(`5-~u%sW}sG1N3RPk3?t6mW3&G1*xj&fN3BS-G40sd>z6(Q#y3FLZ^diUQT- zg!3?!$cwa#bFKvn5Eke#3Pma zV|P8oHQp;z)5HiZdGUA_|H9C$!Ps0EjEZlkRfrjZo)&taO>yz}c3yi69`?Jt;&Ug@ zIS2I|Bo;ZUbB43Tw|{ZNV)ir6{H%v*T* zn#vXZ0;?MP#f)iMw!=+MP`s%bL$3=qG>fs*%zL@>bY67KTkZ*)RzfSOcC}NG`ozUY zaD}zLs@e|uP73C%)rr--`K-8s_>92d8w5Rf*j{K#_H+7Tow;bS1y)U{a4rU?m@qJs zo|^x5H40<*7^cd3TL#P&>`1YACdOg~3>>JPeN`qsYqTEar+p>;Lxx`Xnk|jnDl1`? zp5F<>Yr%g-s))cp4WX00U*4r*MGixPQ%6lqOp=e<^&zIoUwjMB3RQZ?R>asWFZY-= z2qrX3&60lOp`I4KfjVS}w{Fn$oM&<3NSO&{nNSH6LKETN@j21rrjJ;~(_{t})06(O zki4C{L#5a^Rqd~Mr3Rg;@(9J|2A8x^Sq+&I1KqKN_2@?Z_YK10I=RP?t0dXbo6C%{ zKIvLEXCctbBV?}@Hy6)4S+yK$wp1V`g-3a6G)e4y2_TNW1*dR#=6zcRS!FF|#PH?8 zn84OVu99C|57Cm0*};Uc4+qxylGc;uU>(0UlL{}kLh2`SH^rn3rk01Ej`g1O9`0OF z>X&=db9f`n8?4jz@=@7eCRtqmF?uGgs+fGsh&ydgG?y3m6zE^e%RNfv zdVz)UY1?;RYbGa5P4|0w14vq~aKRNYuP5K4I^Jgim!_{f66@cP;Ehi5zC@hf^cK0s zO>dVwn6%E`^#YD2TLt)6DYj7BqQ-x~A85CT-1{aQ?*-WM@BdRO$XV;cyYMF2h;yqH zpH6!z-e-ZN_1(~Qtv$6d&8>gl`gt7&fh|lnaMR~)tou|xc*lY+^*iGP1h(Z|(m!X#v+aLqXj{Yik1cYx^{WaQPAy*Y<5~8SfvJAsdB?y1AM`e^ AcmMzZ literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/user_avatar_1.png b/packages/stream_chat_flutter/test/src/goldens/user_avatar_1.png new file mode 100644 index 0000000000000000000000000000000000000000..189e34326b0cf50fa7a855de17ff4cde6aee8880 GIT binary patch literal 33324 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 client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - when(() => channel.lastMessageAt).thenReturn(lastMessageAt); - when(() => channel.state).thenReturn(channelState); - when(() => channel.client).thenReturn(client); - when(() => channel.isMuted).thenReturn(false); - when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - }); - - await tester.pumpWidget(MaterialApp( - home: StreamChat( - client: client, - child: StreamChannel( - channel: channel, - child: WillPopScope( - onWillPop: () async => false, - child: Scaffold( - body: StreamGalleryFooter( - mediaAttachmentPackages: Message().getAttachmentPackageList(), - ), - ), - ), - ), - ), - )); - - expect(find.byType(StreamSvgIcon), findsNWidgets(2)); - }, - ); -} diff --git a/packages/stream_chat_flutter/test/src/indicators/sending_indicator_test.dart b/packages/stream_chat_flutter/test/src/indicators/sending_indicator_test.dart new file mode 100644 index 00000000..e47b43cd --- /dev/null +++ b/packages/stream_chat_flutter/test/src/indicators/sending_indicator_test.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + testWidgets('StreamSendingIndicator shows an Icon', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: StreamChatThemeData.light(), + child: Scaffold( + body: Center( + child: StreamSendingIndicator( + message: Message(), + ), + ), + ), + ), + ), + ); + + expect(find.byType(Icon), findsOneWidget); + }); + + testGoldens( + 'golden test for StreamSendingIndicator with StreamSvgIcon.checkAll', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: StreamChatThemeData.light(), + child: Scaffold( + body: Center( + child: StreamSendingIndicator( + isMessageRead: true, + message: Message(), + ), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'sending_indicator_0'); + }); + + testGoldens('golden test for StreamSendingIndicator with StreamSvgIcon.check', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: StreamChatThemeData.light(), + child: Scaffold( + body: Center( + child: StreamSendingIndicator( + message: Message(), + ), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'sending_indicator_1'); + }); + + testGoldens( + 'golden test for StreamSendingIndicator with Icon(Icons.access_time)', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: StreamChatThemeData.light(), + child: Scaffold( + body: Center( + child: StreamSendingIndicator( + message: Message(), + ), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'sending_indicator_2'); + }); +} diff --git a/packages/stream_chat_flutter/test/src/typing_indicator_test.dart b/packages/stream_chat_flutter/test/src/indicators/typing_indicator_test.dart similarity index 71% rename from packages/stream_chat_flutter/test/src/typing_indicator_test.dart rename to packages/stream_chat_flutter/test/src/indicators/typing_indicator_test.dart index 27b3f977..63869866 100644 --- a/packages/stream_chat_flutter/test/src/typing_indicator_test.dart +++ b/packages/stream_chat_flutter/test/src/indicators/typing_indicator_test.dart @@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { testWidgets( @@ -22,18 +22,22 @@ void main() { when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); + when(() => channel.extraDataStream).thenAnswer( + (i) => Stream.value({ + 'name': 'test', + }), + ); when(() => channel.extraData).thenReturn({ 'name': 'test', }); - when(() => channelState.membersStream).thenAnswer((i) => Stream.value([ - Member( - userId: 'user-id', - user: User(id: 'user-id'), - ) - ])); + when(() => channelState.membersStream).thenAnswer( + (i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ]), + ); when(() => channelState.members).thenReturn([ Member( userId: 'user-id', @@ -46,22 +50,25 @@ void main() { user: User(id: 'other-user'), ) ]); - when(() => channelState.messagesStream).thenAnswer((i) => Stream.value([ - Message( - text: 'hello', - user: User(id: 'other-user'), - ) - ])); + when(() => channelState.messagesStream).thenAnswer( + (i) => Stream.value([ + Message( + text: 'hello', + user: User(id: 'other-user'), + ) + ]), + ); when(() => channelState.typingEvents).thenAnswer((i) => { User(id: 'other-user', extraData: const {'name': 'demo'}): Event(type: EventType.typingStart), }); - when(() => channelState.typingEventsStream) - .thenAnswer((i) => Stream.value({ - User(id: 'other-user', extraData: const {'name': 'demo'}): - Event(type: EventType.typingStart), - })); + when(() => channelState.typingEventsStream).thenAnswer( + (i) => Stream.value({ + User(id: 'other-user', extraData: const {'name': 'demo'}): + Event(type: EventType.typingStart), + }), + ); const typingKey = Key('typing'); diff --git a/packages/stream_chat_flutter/test/src/unread_indicator_test.dart b/packages/stream_chat_flutter/test/src/indicators/unread_indicator_test.dart similarity index 96% rename from packages/stream_chat_flutter/test/src/unread_indicator_test.dart rename to packages/stream_chat_flutter/test/src/indicators/unread_indicator_test.dart index a470fe74..523681b9 100644 --- a/packages/stream_chat_flutter/test/src/unread_indicator_test.dart +++ b/packages/stream_chat_flutter/test/src/indicators/unread_indicator_test.dart @@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { testWidgets( @@ -20,9 +20,11 @@ void main() { when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); + when(() => channel.extraDataStream).thenAnswer( + (i) => Stream.value({ + 'name': 'test', + }), + ); when(() => channel.extraData).thenReturn({ 'name': 'test', }); diff --git a/packages/stream_chat_flutter/test/src/indicators/upload_progress_indicator_test.dart b/packages/stream_chat_flutter/test/src/indicators/upload_progress_indicator_test.dart new file mode 100644 index 00000000..d7e01dc7 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/indicators/upload_progress_indicator_test.dart @@ -0,0 +1,167 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + testWidgets('StreamUploadProgressIndicator at 0% with no background', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: StreamChatThemeData.light(), + child: const Scaffold( + body: Center( + child: StreamUploadProgressIndicator( + total: 100, + uploaded: 0, + showBackground: false, + ), + ), + ), + ), + ), + ); + + expect(find.text('0%'), findsOneWidget); + }); + + testWidgets('StreamUploadProgressIndicator at 50% with no background', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: StreamChatThemeData.light(), + child: const Scaffold( + body: Center( + child: StreamUploadProgressIndicator( + total: 100, + uploaded: 50, + showBackground: false, + ), + ), + ), + ), + ), + ); + + expect(find.text('50%'), findsOneWidget); + }); + + testWidgets('StreamUploadProgressIndicator at 100% with no background', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: StreamChatThemeData.light(), + child: const Scaffold( + body: Center( + child: StreamUploadProgressIndicator( + total: 100, + uploaded: 100, + showBackground: false, + ), + ), + ), + ), + ), + ); + + expect(find.text('100%'), findsOneWidget); + }); + + testWidgets('StreamUploadProgressIndicator at 50% with background', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: StreamChatThemeData.light(), + child: const Scaffold( + body: Center( + child: StreamUploadProgressIndicator( + total: 100, + uploaded: 50, + ), + ), + ), + ), + ), + ); + + final backgroundColor = + ((find.byType(DecoratedBox).evaluate().first.widget as DecoratedBox) + .decoration as BoxDecoration) + .color; + + expect(const Color(0x99000000), backgroundColor); + }); + + testGoldens( + 'golden test for StreamUploadProgressIndicator at 0% with background', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: StreamChatThemeData.light(), + child: const Scaffold( + body: Center( + child: StreamUploadProgressIndicator( + total: 100, + uploaded: 0, + ), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'upload_progress_indicator_0', + customPump: (widget) => widget.pump(const Duration(seconds: 3))); + }); + + testGoldens( + 'golden test for StreamUploadProgressIndicator at 50% with background', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: StreamChatThemeData.light(), + child: const Scaffold( + body: Center( + child: StreamUploadProgressIndicator( + total: 100, + uploaded: 50, + ), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'upload_progress_indicator_1', + customPump: (widget) => widget.pump(const Duration(seconds: 3))); + }); + + testGoldens( + 'golden test for StreamUploadProgressIndicator at 100% with background', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: StreamChatThemeData.light(), + child: const Scaffold( + body: Center( + child: StreamUploadProgressIndicator( + total: 100, + uploaded: 100, + ), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'upload_progress_indicator_2', + customPump: (widget) => widget.pump(const Duration(seconds: 3))); + }); +} diff --git a/packages/stream_chat_flutter/test/src/keyboard_shortcuts/keyboard_shortcut_runner_test.dart b/packages/stream_chat_flutter/test/src/keyboard_shortcuts/keyboard_shortcut_runner_test.dart new file mode 100644 index 00000000..269e8320 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/keyboard_shortcuts/keyboard_shortcut_runner_test.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + testWidgets('KeyboardShortcutRunner onEnterKeypress works', (tester) async { + var count = 0; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: KeyboardShortcutRunner( + onEnterKeypress: () { + count++; + }, + onEscapeKeypress: () {}, + child: const TextField(), + ), + ), + ), + ), + ); + + final textField = find.byType(TextField); + await tester.tap(textField); + await tester.enterText(textField, 'Test'); + await tester.sendKeyDownEvent(LogicalKeyboardKey.enter); + await tester.pumpAndSettle(); + expect(count, 1); + }); + + testWidgets('KeyboardShortcutRunner onEscapeKeypress works', (tester) async { + final controller = TextEditingController(); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: KeyboardShortcutRunner( + onEnterKeypress: () {}, + onEscapeKeypress: controller.clear, + child: TextField( + controller: controller, + ), + ), + ), + ), + ), + ); + + final textField = find.byType(TextField); + await tester.tap(textField); + await tester.enterText(textField, 'Test'); + await tester.sendKeyDownEvent(LogicalKeyboardKey.escape); + await tester.pumpAndSettle(); + expect(controller.text, ''); + }); +} diff --git a/packages/stream_chat_flutter/test/src/default_translations_test.dart b/packages/stream_chat_flutter/test/src/localization/default_translations_test.dart similarity index 99% rename from packages/stream_chat_flutter/test/src/default_translations_test.dart rename to packages/stream_chat_flutter/test/src/localization/default_translations_test.dart index 14a4a318..eefa3593 100644 --- a/packages/stream_chat_flutter/test/src/default_translations_test.dart +++ b/packages/stream_chat_flutter/test/src/localization/default_translations_test.dart @@ -56,7 +56,6 @@ void main() { expect(translations.instantCommandsLabel, isNotNull); expect(translations.fileTooLargeAfterCompressionError(33), isNotNull); expect(translations.fileTooLargeError(33), isNotNull); - expect(translations.emojiMatchingQueryText('sahil'), isNotNull); expect(translations.addAFileLabel, isNotNull); expect(translations.photoFromCameraLabel, isNotNull); expect(translations.uploadAFileLabel, isNotNull); diff --git a/packages/stream_chat_flutter/test/src/media_list_view_controller_test.dart b/packages/stream_chat_flutter/test/src/media_list_view_controller_test.dart deleted file mode 100644 index c8dd1d7d..00000000 --- a/packages/stream_chat_flutter/test/src/media_list_view_controller_test.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:stream_chat_flutter/src/media_list_view_controller.dart'; - -void main() { - test('should update media', () { - final controller = MediaListViewController(); - - expect(controller.shouldUpdateMedia, false); - - controller.updateMedia(newValue: true); - expect(controller.shouldUpdateMedia, true); - - controller.dispose(); - }); - - test('should notify listeners on update media', () { - final controller = MediaListViewController(); - - var callCount = 0; - void updateCallsSpy() => callCount++; - - controller.addListener(updateCallsSpy); - - expect(callCount, 0); - controller.updateMedia(newValue: false); - expect(controller.shouldUpdateMedia, false); - expect(callCount, 1); - - controller.updateMedia(newValue: true); - expect(controller.shouldUpdateMedia, true); - expect(callCount, 2); - - controller - ..removeListener(updateCallsSpy) - ..dispose(); - }); -} diff --git a/packages/stream_chat_flutter/test/src/message_action_modal_test.dart b/packages/stream_chat_flutter/test/src/message_actions_modal/message_actions_modal_test.dart similarity index 94% rename from packages/stream_chat_flutter/test/src/message_action_modal_test.dart rename to packages/stream_chat_flutter/test/src/message_actions_modal/message_actions_modal_test.dart index e0f8dcb3..59077817 100644 --- a/packages/stream_chat_flutter/test/src/message_action_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/message_actions_modal/message_actions_modal_test.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat_flutter/src/message_actions_modal.dart'; +import 'package:stream_chat_flutter/src/message_actions_modal/message_actions_modal.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { setUpAll(() { @@ -34,7 +34,7 @@ void main() { child: SizedBox( child: StreamChannel( channel: channel, - child: StreamMessageActionsModal( + child: MessageActionsModal( message: Message( text: 'test', user: User( @@ -47,9 +47,6 @@ void main() { key: Key('MessageWidget'), ), messageTheme: streamTheme.ownMessageTheme, - showThreadReplyMessage: true, - showEditMessage: true, - showDeleteMessage: true, ), ), ), @@ -88,7 +85,7 @@ void main() { child: SizedBox( child: StreamChannel( channel: channel, - child: StreamMessageActionsModal( + child: MessageActionsModal( showCopyMessage: false, showReplyMessage: false, showThreadReplyMessage: false, @@ -134,16 +131,17 @@ void main() { final streamTheme = StreamChatThemeData.fromTheme(themeData); var tapped = false; - await tester.pumpWidget( - MaterialApp( - theme: themeData, - home: StreamChat( - streamChatThemeData: streamTheme, - client: client, - child: SizedBox( + await tester.pumpWidget(MaterialApp( + theme: themeData, + home: StreamChat( + streamChatThemeData: streamTheme, + client: client, + child: SizedBox( + child: StreamChannel( + channel: channel, child: StreamChannel( channel: channel, - child: StreamMessageActionsModal( + child: MessageActionsModal( messageWidget: const Text('test'), message: Message( text: 'test', @@ -166,7 +164,7 @@ void main() { ), ), ), - ); + )); await tester.pumpAndSettle(); @@ -203,7 +201,7 @@ void main() { child: SizedBox( child: StreamChannel( channel: channel, - child: StreamMessageActionsModal( + child: MessageActionsModal( messageWidget: const Text('test'), onReplyTap: (m) { tapped = true; @@ -254,7 +252,7 @@ void main() { child: SizedBox( child: StreamChannel( channel: channel, - child: StreamMessageActionsModal( + child: MessageActionsModal( messageWidget: const Text('test'), onThreadReplyTap: (m) { tapped = true; @@ -267,7 +265,6 @@ void main() { status: MessageSendingStatus.sent, ), messageTheme: streamTheme.ownMessageTheme, - showThreadReplyMessage: true, ), ), ), @@ -309,7 +306,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: StreamMessageActionsModal( + child: MessageActionsModal( messageWidget: const Text('test'), message: Message( text: 'test', @@ -318,7 +315,6 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, - showEditMessage: true, ), ), ), @@ -359,7 +355,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: StreamMessageActionsModal( + child: MessageActionsModal( messageWidget: const Text('test'), editMessageInputBuilder: (context, m) => const Text('test'), message: Message( @@ -369,7 +365,6 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, - showEditMessage: true, ), ), ), @@ -412,7 +407,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: StreamMessageActionsModal( + child: MessageActionsModal( messageWidget: const Text('test'), onCopyTap: (m) => tapped = true, message: Message( @@ -462,7 +457,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: StreamMessageActionsModal( + child: MessageActionsModal( messageWidget: const Text('test'), message: Message( status: MessageSendingStatus.failed, @@ -512,7 +507,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: StreamMessageActionsModal( + child: MessageActionsModal( messageWidget: const Text('test'), message: Message( status: MessageSendingStatus.failed_update, @@ -560,7 +555,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: StreamMessageActionsModal( + child: MessageActionsModal( messageWidget: const Text('test'), message: Message( id: 'testid', @@ -570,7 +565,6 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, - showFlagButton: true, ), ), ), @@ -617,7 +611,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: StreamMessageActionsModal( + child: MessageActionsModal( messageWidget: const Text('test'), message: Message( id: 'testid', @@ -627,7 +621,6 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, - showFlagButton: true, ), ), ), @@ -674,7 +667,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: StreamMessageActionsModal( + child: MessageActionsModal( messageWidget: const Text('test'), message: Message( id: 'testid', @@ -684,7 +677,6 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, - showFlagButton: true, ), ), ), @@ -729,7 +721,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: StreamMessageActionsModal( + child: MessageActionsModal( messageWidget: const Text('test'), message: Message( id: 'testid', @@ -739,7 +731,6 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, - showDeleteMessage: true, ), ), ), @@ -786,7 +777,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: StreamMessageActionsModal( + child: MessageActionsModal( messageWidget: const Text('test'), message: Message( id: 'testid', @@ -796,7 +787,6 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, - showDeleteMessage: true, ), ), ), diff --git a/packages/stream_chat_flutter/test/src/message_input/attachment_button_test.dart b/packages/stream_chat_flutter/test/src/message_input/attachment_button_test.dart new file mode 100644 index 00000000..19a2e659 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_input/attachment_button_test.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:stream_chat_flutter/src/message_input/attachment_button.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + testWidgets('SendButton onPressed works', (tester) async { + var count = 0; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: AttachmentButton( + color: StreamChatThemeData.light() + .messageInputTheme + .actionButtonIdleColor!, + onPressed: () { + count++; + }, + ), + ), + ), + ), + ); + + final button = find.byType(IconButton); + expect(button, findsOneWidget); + expect(find.byType(StreamSvgIcon), findsOneWidget); + await tester.tap(button); + expect(count, 1); + }); + + testGoldens('golden test for AttachmentButton', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: AttachmentButton( + color: StreamChatThemeData.light() + .messageInputTheme + .actionButtonIdleColor!, + onPressed: () {}, + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'attachment_button_0'); + }); +} diff --git a/packages/stream_chat_flutter/test/src/message_input/clear_input_item_test.dart b/packages/stream_chat_flutter/test/src/message_input/clear_input_item_test.dart new file mode 100644 index 00000000..a2d1e302 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_input/clear_input_item_test.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:stream_chat_flutter/src/message_input/clear_input_item_button.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + testWidgets('ClearInputItemButton onPressed works', (tester) async { + var count = 0; + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: StreamChatThemeData.light(), + child: Scaffold( + body: Center( + child: ClearInputItemButton( + onTap: () { + count++; + }, + ), + ), + ), + ), + ), + ); + + final button = find.byType(RawMaterialButton); + expect(button, findsOneWidget); + expect(find.byType(StreamSvgIcon), findsOneWidget); + await tester.tap(button); + expect(count, 1); + }); + + testGoldens('golden test for ClearInputItemButton', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: StreamChatThemeData.light(), + child: Scaffold( + body: Center( + child: ClearInputItemButton( + onTap: () {}, + ), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'clear_input_item_0'); + }); +} diff --git a/packages/stream_chat_flutter/test/src/message_input/command_button_test.dart b/packages/stream_chat_flutter/test/src/message_input/command_button_test.dart new file mode 100644 index 00000000..fd712f54 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_input/command_button_test.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:stream_chat_flutter/src/message_input/command_button.dart'; + +void main() { + testWidgets('CommandButton onPressed works', (tester) async { + var count = 0; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: CommandButton( + color: Colors.red, + onPressed: () { + count++; + }, + ), + ), + ), + ), + ); + + final button = find.byType(IconButton); + expect(button, findsOneWidget); + await tester.tap(button); + expect(count, 1); + }); + + testGoldens('golden test for CommandButton', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: CommandButton( + color: Colors.red, + onPressed: () {}, + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'command_button_0'); + }); +} diff --git a/packages/stream_chat_flutter/test/src/message_input/countdown_button_test.dart b/packages/stream_chat_flutter/test/src/message_input/countdown_button_test.dart new file mode 100644 index 00000000..a8109260 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_input/countdown_button_test.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + testWidgets('CountdownButton works', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: StreamChatThemeData.light(), + child: const Scaffold( + body: Center( + child: StreamCountdownButton(count: 5), + ), + ), + ), + ), + ); + + expect(find.text('5'), findsOneWidget); + }); + + testGoldens('golden test for CountdownButton', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: StreamChatThemeData.light(), + child: const Scaffold( + body: Center( + child: StreamCountdownButton(count: 5), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'countdown_button_0'); + }); +} diff --git a/packages/stream_chat_flutter/test/src/message_input/dm_checkbox_test.dart b/packages/stream_chat_flutter/test/src/message_input/dm_checkbox_test.dart new file mode 100644 index 00000000..f33f9750 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_input/dm_checkbox_test.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:stream_chat_flutter/src/message_input/dm_checkbox.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + testWidgets('DmCheckbox onTap works', (tester) async { + var count = 0; + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: StreamChatThemeData.light(), + child: Scaffold( + body: Center( + child: DmCheckbox( + foregroundDecoration: BoxDecoration( + border: Border.all( + color: StreamChatThemeData.light() + .colorTheme + .textHighEmphasis + .withOpacity(0.5), + width: 2, + ), + borderRadius: BorderRadius.circular(3), + ), + color: StreamChatThemeData.light().colorTheme.accentPrimary, + onTap: () { + count++; + }, + crossFadeState: CrossFadeState.showFirst, + ), + ), + ), + ), + ), + ); + + expect(find.byType(AnimatedCrossFade), findsOneWidget); + final checkbox = find.byType(InkWell); + await tester.tap(checkbox); + await tester.pumpAndSettle(); + expect(count, 1); + }); + + testGoldens('golden test for checked DmCheckbox with border', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: StreamChatThemeData.light(), + child: Scaffold( + body: Center( + child: DmCheckbox( + foregroundDecoration: BoxDecoration( + border: Border.all( + color: StreamChatThemeData.light() + .colorTheme + .textHighEmphasis + .withOpacity(0.5), + width: 2, + ), + borderRadius: BorderRadius.circular(3), + ), + color: StreamChatThemeData.light().colorTheme.accentPrimary, + onTap: () {}, + crossFadeState: CrossFadeState.showFirst, + ), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'dm_checkbox_0'); + }); + + testGoldens('golden test for checked DmCheckbox without border', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: StreamChatThemeData.light(), + child: Scaffold( + body: Center( + child: DmCheckbox( + foregroundDecoration: BoxDecoration( + borderRadius: BorderRadius.circular(3), + ), + color: StreamChatThemeData.light().colorTheme.accentPrimary, + onTap: () {}, + crossFadeState: CrossFadeState.showFirst, + ), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'dm_checkbox_1'); + }); + + testGoldens('golden test for unchecked DmCheckbox with border', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: StreamChatThemeData.light(), + child: Scaffold( + body: Center( + child: DmCheckbox( + foregroundDecoration: BoxDecoration( + border: Border.all( + color: StreamChatThemeData.light() + .colorTheme + .textHighEmphasis + .withOpacity(0.5), + width: 2, + ), + borderRadius: BorderRadius.circular(3), + ), + color: StreamChatThemeData.light().colorTheme.barsBg, + onTap: () {}, + crossFadeState: CrossFadeState.showSecond, + ), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'dm_checkbox_2'); + }); +} diff --git a/packages/stream_chat_flutter/test/src/message_input_test.dart b/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart similarity index 75% rename from packages/stream_chat_flutter/test/src/message_input_test.dart rename to packages/stream_chat_flutter/test/src/message_input/message_input_test.dart index cdedc818..1fe25ac3 100644 --- a/packages/stream_chat_flutter/test/src/message_input_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart @@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { testWidgets( @@ -22,18 +22,22 @@ void main() { when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); + when(() => channel.extraDataStream).thenAnswer( + (i) => Stream.value({ + 'name': 'test', + }), + ); when(() => channel.extraData).thenReturn({ 'name': 'test', }); - when(() => channelState.membersStream).thenAnswer((i) => Stream.value([ - Member( - userId: 'user-id', - user: User(id: 'user-id'), - ) - ])); + when(() => channelState.membersStream).thenAnswer( + (i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ]), + ); when(() => channelState.members).thenReturn([ Member( userId: 'user-id', @@ -46,12 +50,14 @@ void main() { user: User(id: 'other-user'), ) ]); - when(() => channelState.messagesStream).thenAnswer((i) => Stream.value([ - Message( - text: 'hello', - user: User(id: 'other-user'), - ) - ])); + when(() => channelState.messagesStream).thenAnswer( + (i) => Stream.value([ + Message( + text: 'hello', + user: User(id: 'other-user'), + ) + ]), + ); await tester.pumpWidget(MaterialApp( home: StreamChat( @@ -88,18 +94,22 @@ void main() { when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); + when(() => channel.extraDataStream).thenAnswer( + (i) => Stream.value({ + 'name': 'test', + }), + ); when(() => channel.extraData).thenReturn({ 'name': 'test', }); - when(() => channelState.membersStream).thenAnswer((i) => Stream.value([ - Member( - userId: 'user-id', - user: User(id: 'user-id'), - ) - ])); + when(() => channelState.membersStream).thenAnswer( + (i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ]), + ); when(() => channelState.members).thenReturn([ Member( userId: 'user-id', @@ -112,12 +122,14 @@ void main() { user: User(id: 'other-user'), ) ]); - when(() => channelState.messagesStream).thenAnswer((i) => Stream.value([ - Message( - text: 'hello', - user: User(id: 'other-user'), - ) - ])); + when(() => channelState.messagesStream).thenAnswer( + (i) => Stream.value([ + Message( + text: 'hello', + user: User(id: 'other-user'), + ) + ]), + ); await tester.pumpWidget(MaterialApp( home: StreamChat( diff --git a/packages/stream_chat_flutter/test/src/message_list_view/floating_date_divider_test.dart b/packages/stream_chat_flutter/test/src/message_list_view/floating_date_divider_test.dart new file mode 100644 index 00000000..a6403331 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_list_view/floating_date_divider_test.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; +import 'package:stream_chat_flutter/src/message_list_view/floating_date_divider.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + testWidgets('FloatingDateDivider', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Stack( + children: [ + FloatingDateDivider( + reverse: false, + itemCount: 3, + itemPositionListener: ItemPositionsListener.create(), + messages: [ + Message(), + Message(), + Message(), + ], + ), + ], + ), + ), + ), + ); + + expect(find.byType(Positioned), findsOneWidget); + }); +} diff --git a/packages/stream_chat_flutter/test/src/message_list_view_test.dart b/packages/stream_chat_flutter/test/src/message_list_view/message_list_view_test.dart similarity index 96% rename from packages/stream_chat_flutter/test/src/message_list_view_test.dart rename to packages/stream_chat_flutter/test/src/message_list_view/message_list_view_test.dart index 240dda84..7bb4a954 100644 --- a/packages/stream_chat_flutter/test/src/message_list_view_test.dart +++ b/packages/stream_chat_flutter/test/src/message_list_view/message_list_view_test.dart @@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { late StreamChatClient client; @@ -121,10 +121,11 @@ void main() { expect(find.byType(StreamMessageListView), findsOneWidget); expect(find.byKey(nonEmptyWidgetKey), findsOneWidget); expect( - find.byWidgetPredicate( - findBackground, - description: 'findBackground', - ), - findsOneWidget); + find.byWidgetPredicate( + findBackground, + description: 'findBackground', + ), + findsOneWidget, + ); }); } diff --git a/packages/stream_chat_flutter/test/src/message_list_view/thread_separator_test.dart b/packages/stream_chat_flutter/test/src/message_list_view/thread_separator_test.dart new file mode 100644 index 00000000..c0cfcb50 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_list_view/thread_separator_test.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/src/message_list_view/thread_separator.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + testWidgets('ThreadSeparator', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: StreamChatTheme( + data: StreamChatThemeData.light(), + child: Scaffold( + body: Center( + child: ThreadSeparator( + parentMessage: Message(), + ), + ), + ), + ), + ), + ); + + expect(find.byType(DecoratedBox), findsOneWidget); + expect(find.byType(Text), findsOneWidget); + }); +} diff --git a/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart b/packages/stream_chat_flutter/test/src/message_reactions_modal/message_reactions_modal_test.dart similarity index 94% rename from packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart rename to packages/stream_chat_flutter/test/src/message_reactions_modal/message_reactions_modal_test.dart index 31812bf2..54f3675b 100644 --- a/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/message_reactions_modal/message_reactions_modal_test.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat_flutter/src/message_reactions_modal.dart'; -import 'package:stream_chat_flutter/src/reaction_bubble.dart'; +import 'package:stream_chat_flutter/src/message_widget/reactions/message_reactions_modal.dart'; +import 'package:stream_chat_flutter/src/message_widget/reactions/reaction_bubble.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { testWidgets( diff --git a/packages/stream_chat_flutter/test/src/deleted_message_test.dart b/packages/stream_chat_flutter/test/src/message_widget/deleted_message_test.dart similarity index 87% rename from packages/stream_chat_flutter/test/src/deleted_message_test.dart rename to packages/stream_chat_flutter/test/src/message_widget/deleted_message_test.dart index 171b993d..c7dbbf72 100644 --- a/packages/stream_chat_flutter/test/src/deleted_message_test.dart +++ b/packages/stream_chat_flutter/test/src/message_widget/deleted_message_test.dart @@ -4,7 +4,7 @@ import 'package:golden_toolkit/golden_toolkit.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { testWidgets( @@ -16,21 +16,23 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - await tester.pumpWidget(MaterialApp( - home: StreamChat( - client: client, - child: const Scaffold( - body: StreamDeletedMessage( - messageTheme: StreamMessageThemeData( - createdAtStyle: TextStyle( - color: Colors.black, + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: const Scaffold( + body: StreamDeletedMessage( + messageTheme: StreamMessageThemeData( + createdAtStyle: TextStyle( + color: Colors.black, + ), + messageTextStyle: TextStyle(), ), - messageTextStyle: TextStyle(), ), ), ), ), - )); + ); expect(find.text('Message deleted'), findsOneWidget); }, @@ -50,9 +52,11 @@ void main() { when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); + when(() => channel.extraDataStream).thenAnswer( + (i) => Stream.value({ + 'name': 'test', + }), + ); when(() => channel.extraData).thenReturn({ 'name': 'test', }); @@ -103,9 +107,11 @@ void main() { when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); + when(() => channel.extraDataStream).thenAnswer( + (i) => Stream.value({ + 'name': 'test', + }), + ); when(() => channel.extraData).thenReturn({ 'name': 'test', }); @@ -156,9 +162,11 @@ void main() { when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); + when(() => channel.extraDataStream).thenAnswer( + (i) => Stream.value({ + 'name': 'test', + }), + ); when(() => channel.extraData).thenReturn({ 'name': 'test', }); diff --git a/packages/stream_chat_flutter/test/src/message_text_test.dart b/packages/stream_chat_flutter/test/src/message_widget/message_text_test.dart similarity index 97% rename from packages/stream_chat_flutter/test/src/message_text_test.dart rename to packages/stream_chat_flutter/test/src/message_widget/message_text_test.dart index 8f2d4c10..8d205221 100644 --- a/packages/stream_chat_flutter/test/src/message_text_test.dart +++ b/packages/stream_chat_flutter/test/src/message_widget/message_text_test.dart @@ -5,8 +5,8 @@ import 'package:golden_toolkit/golden_toolkit.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; -import 'simple_frame.dart'; +import '../mocks.dart'; +import '../simple_frame.dart'; void expectTextStrings(Iterable widgets, List strings) { var currentString = 0; @@ -197,9 +197,11 @@ void main() { when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); + when(() => channel.extraDataStream).thenAnswer( + (i) => Stream.value({ + 'name': 'test', + }), + ); when(() => channel.extraData).thenReturn({ 'name': 'test', }); diff --git a/packages/stream_chat_flutter/test/src/message_widget/username_test.dart b/packages/stream_chat_flutter/test/src/message_widget/username_test.dart new file mode 100644 index 00000000..bde411cb --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_widget/username_test.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/src/message_widget/username.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + testWidgets('Username', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: Username( + message: Message(), + messageTheme: StreamChatThemeData.light().ownMessageTheme, + ), + ), + ), + ), + ); + + expect(find.byType(Text), findsOneWidget); + }); +} diff --git a/packages/stream_chat_flutter/test/src/back_button_test.dart b/packages/stream_chat_flutter/test/src/misc/back_button_test.dart similarity index 82% rename from packages/stream_chat_flutter/test/src/back_button_test.dart rename to packages/stream_chat_flutter/test/src/misc/back_button_test.dart index f6ec0fc4..c4ae6efc 100644 --- a/packages/stream_chat_flutter/test/src/back_button_test.dart +++ b/packages/stream_chat_flutter/test/src/misc/back_button_test.dart @@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { testWidgets( @@ -14,14 +14,16 @@ void main() { MaterialApp( home: const Material(child: Text('Home')), routes: { - '/next': (BuildContext context) => Material( - child: Center( - child: StreamChatTheme( - data: StreamChatThemeData.fromTheme(theme), - child: const StreamBackButton(), - ), + '/next': (BuildContext context) { + return Material( + child: Center( + child: StreamChatTheme( + data: StreamChatThemeData.fromTheme(theme), + child: const StreamBackButton(), ), ), + ); + }, }, ), ); @@ -75,16 +77,18 @@ void main() { MaterialApp( home: const Material(child: Text('Home')), routes: { - '/next': (BuildContext context) => Material( - child: Center( - child: StreamChatTheme( - data: StreamChatThemeData.fromTheme(theme), - child: StreamBackButton( - onPressed: () => customCallbackWasCalled = true, - ), + '/next': (BuildContext context) { + return Material( + child: Center( + child: StreamChatTheme( + data: StreamChatThemeData.fromTheme(theme), + child: StreamBackButton( + onPressed: () => customCallbackWasCalled = true, ), ), ), + ); + }, }, ), ); @@ -128,7 +132,7 @@ void main() { child: StreamChat( client: client, child: const StreamBackButton( - showUnreads: true, + showUnreadCount: true, ), ), ), diff --git a/packages/stream_chat_flutter/test/src/date_divider_test.dart b/packages/stream_chat_flutter/test/src/misc/date_divider_test.dart similarity index 70% rename from packages/stream_chat_flutter/test/src/date_divider_test.dart rename to packages/stream_chat_flutter/test/src/misc/date_divider_test.dart index 59ba4be1..0b88ffc7 100644 --- a/packages/stream_chat_flutter/test/src/date_divider_test.dart +++ b/packages/stream_chat_flutter/test/src/misc/date_divider_test.dart @@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { testWidgets( @@ -15,16 +15,18 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - await tester.pumpWidget(MaterialApp( - home: StreamChat( - client: client, - child: Scaffold( - body: StreamDateDivider( - dateTime: DateTime.now(), + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: Scaffold( + body: StreamDateDivider( + dateTime: DateTime.now(), + ), ), ), ), - )); + ); expect(find.text('Today'), findsOneWidget); }, diff --git a/packages/stream_chat_flutter/test/src/info_tile_test.dart b/packages/stream_chat_flutter/test/src/misc/info_tile_test.dart similarity index 58% rename from packages/stream_chat_flutter/test/src/info_tile_test.dart rename to packages/stream_chat_flutter/test/src/misc/info_tile_test.dart index e0687964..9cdaa0ee 100644 --- a/packages/stream_chat_flutter/test/src/info_tile_test.dart +++ b/packages/stream_chat_flutter/test/src/misc/info_tile_test.dart @@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { testWidgets( @@ -16,22 +16,24 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - await tester.pumpWidget(MaterialApp( - home: StreamChat( - client: client, - child: const Scaffold( - body: Portal( - child: SizedBox( - child: StreamInfoTile( - showMessage: true, - message: 'message', - child: Text('test'), + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: const Scaffold( + body: Portal( + child: SizedBox( + child: StreamInfoTile( + showMessage: true, + message: 'message', + child: Text('test'), + ), ), ), ), ), ), - )); + ); expect(find.text('message'), findsOneWidget); }, @@ -46,22 +48,24 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - await tester.pumpWidget(MaterialApp( - home: StreamChat( - client: client, - child: const Scaffold( - body: Portal( - child: SizedBox( - child: StreamInfoTile( - showMessage: false, - message: 'message', - child: Text('test'), + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: const Scaffold( + body: Portal( + child: SizedBox( + child: StreamInfoTile( + showMessage: false, + message: 'message', + child: Text('test'), + ), ), ), ), ), ), - )); + ); expect(find.text('message'), findsNothing); }, diff --git a/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart b/packages/stream_chat_flutter/test/src/misc/reaction_bubble_test.dart similarity index 98% rename from packages/stream_chat_flutter/test/src/reaction_bubble_test.dart rename to packages/stream_chat_flutter/test/src/misc/reaction_bubble_test.dart index e5234a6d..7c14912c 100644 --- a/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart +++ b/packages/stream_chat_flutter/test/src/misc/reaction_bubble_test.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:golden_toolkit/golden_toolkit.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat_flutter/src/reaction_bubble.dart'; +import 'package:stream_chat_flutter/src/message_widget/reactions/reaction_bubble.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { testGoldens( diff --git a/packages/stream_chat_flutter/test/src/system_message_test.dart b/packages/stream_chat_flutter/test/src/misc/system_message_test.dart similarity index 92% rename from packages/stream_chat_flutter/test/src/system_message_test.dart rename to packages/stream_chat_flutter/test/src/misc/system_message_test.dart index c723ff24..8c4026d8 100644 --- a/packages/stream_chat_flutter/test/src/system_message_test.dart +++ b/packages/stream_chat_flutter/test/src/misc/system_message_test.dart @@ -4,7 +4,7 @@ import 'package:golden_toolkit/golden_toolkit.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { testWidgets( @@ -21,9 +21,11 @@ void main() { when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); + when(() => channel.extraDataStream).thenAnswer( + (i) => Stream.value({ + 'name': 'test', + }), + ); when(() => channel.extraData).thenReturn({ 'name': 'test', }); @@ -72,9 +74,11 @@ void main() { when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); + when(() => channel.extraDataStream).thenAnswer( + (i) => Stream.value({ + 'name': 'test', + }), + ); when(() => channel.extraData).thenReturn({ 'name': 'test', }); @@ -124,9 +128,11 @@ void main() { when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); + when(() => channel.extraDataStream).thenAnswer( + (i) => Stream.value({ + 'name': 'test', + }), + ); when(() => channel.extraData).thenReturn({ 'name': 'test', }); diff --git a/packages/stream_chat_flutter/test/src/thread_header_test.dart b/packages/stream_chat_flutter/test/src/misc/thread_header_test.dart similarity index 88% rename from packages/stream_chat_flutter/test/src/thread_header_test.dart rename to packages/stream_chat_flutter/test/src/misc/thread_header_test.dart index e347803e..51a246f2 100644 --- a/packages/stream_chat_flutter/test/src/thread_header_test.dart +++ b/packages/stream_chat_flutter/test/src/misc/thread_header_test.dart @@ -5,7 +5,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { testWidgets( @@ -29,12 +29,14 @@ void main() { when(() => channelState.unreadCount).thenReturn(1); when(() => channelState.unreadCountStream) .thenAnswer((i) => Stream.value(1)); - when(() => channelState.membersStream).thenAnswer((i) => Stream.value([ - Member( - userId: 'user-id', - user: User(id: 'user-id'), - ) - ])); + when(() => channelState.membersStream).thenAnswer( + (i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ]), + ); when(() => channelState.members).thenReturn([ Member( userId: 'user-id', @@ -85,21 +87,25 @@ void main() { when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); + when(() => channel.extraDataStream).thenAnswer( + (i) => Stream.value({ + 'name': 'test', + }), + ); when(() => channel.extraData).thenReturn({ 'name': 'test', }); when(() => channelState.unreadCount).thenReturn(1); when(() => channelState.unreadCountStream) .thenAnswer((i) => Stream.value(1)); - when(() => channelState.membersStream).thenAnswer((i) => Stream.value([ - Member( - userId: 'user-id', - user: User(id: 'user-id'), - ) - ])); + when(() => channelState.membersStream).thenAnswer( + (i) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ) + ]), + ); when(() => channelState.members).thenReturn([ Member( userId: 'user-id', diff --git a/packages/stream_chat_flutter/test/src/mocks.dart b/packages/stream_chat_flutter/test/src/mocks.dart index 1fa1e99b..c8e1a4b4 100644 --- a/packages/stream_chat_flutter/test/src/mocks.dart +++ b/packages/stream_chat_flutter/test/src/mocks.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/src/video/vlc/vlc_manager_desktop.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; class MockClient extends Mock implements StreamChatClient { MockClient() { @@ -30,6 +31,8 @@ class MockChannelState extends Mock implements ChannelClientState { MockChannelState() { when(() => typingEvents).thenReturn({}); when(() => typingEventsStream).thenAnswer((_) => Stream.value({})); + when(() => unreadCount).thenReturn(0); + when(() => read).thenReturn([]); } } @@ -38,3 +41,21 @@ class MockNavigatorObserver extends Mock implements NavigatorObserver {} class MockVoidCallback extends Mock { void call(); } + +class MockAttachmentHandler extends Mock implements StreamAttachmentHandler {} + +class MockMember extends Mock implements Member {} + +class MockUser extends Mock implements User {} + +class MockOwnUser extends Mock implements OwnUser {} + +class MockAttachment extends Mock implements Attachment {} + +class MockVlcManagerDesktop extends Mock implements VlcManagerDesktop {} + +class MockStreamMemberListController extends Mock + implements StreamMemberListController { + @override + PagedValue value = const PagedValue.loading(); +} diff --git a/packages/stream_chat_flutter/test/src/scroll_view/member_scroll_view/stream_member_list_view_test.dart b/packages/stream_chat_flutter/test/src/scroll_view/member_scroll_view/stream_member_list_view_test.dart new file mode 100644 index 00000000..b5275a97 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/scroll_view/member_scroll_view/stream_member_list_view_test.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../../mocks.dart'; + +void main() { + late StreamChatClient client; + late Channel channel; + late ChannelClientState channelClientState; + late ClientState clientState; + + setUp(() { + client = MockClient(); + clientState = MockClientState(); + when(() => client.state).thenAnswer((_) => clientState); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'testid')); + when(() => clientState.currentUserStream) + .thenAnswer((_) => Stream.value(OwnUser(id: 'testid'))); + channel = MockChannel(); + when(() => channel.on(any(), any(), any(), any())) + .thenAnswer((_) => const Stream.empty()); + channelClientState = MockChannelState(); + when(() => channel.client).thenReturn(client); + when(() => channel.state).thenReturn(channelClientState); + + when(() => channelClientState.membersStream) + .thenAnswer((_) => const Stream.empty()); + when(() => channelClientState.members).thenReturn([]); + }); + + testWidgets('renders empty member list view', (tester) async { + const emptyWidgetKey = Key('empty_widget'); + final controller = MockStreamMemberListController(); + + when(controller.doInitialLoad).thenAnswer((_) async { + controller.value = const PagedValue(items: []); + }); + + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: StreamMemberListView( + emptyBuilder: (_) => Container(key: emptyWidgetKey), + controller: controller, + ), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.byType(StreamMemberListView), findsOneWidget); + expect(find.byKey(emptyWidgetKey), findsOneWidget); + }); +} diff --git a/packages/stream_chat_flutter/test/src/simple_frame.dart b/packages/stream_chat_flutter/test/src/simple_frame.dart index aad15b63..830ecbf0 100644 --- a/packages/stream_chat_flutter/test/src/simple_frame.dart +++ b/packages/stream_chat_flutter/test/src/simple_frame.dart @@ -6,12 +6,14 @@ class SimpleFrame extends StatelessWidget { final Widget child; @override - Widget build(BuildContext context) => Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: const Color(0xFFFFFFFF), - border: Border.all(color: const Color(0xFF9E9E9E)), - ), - child: child, - ); + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: const Color(0xFFFFFFFF), + border: Border.all(color: const Color(0xFF9E9E9E)), + ), + child: child, + ); + } } diff --git a/packages/stream_chat_flutter/test/src/stream_chat_configuration_test.dart b/packages/stream_chat_flutter/test/src/stream_chat_configuration_test.dart new file mode 100644 index 00000000..d9db650a --- /dev/null +++ b/packages/stream_chat_flutter/test/src/stream_chat_configuration_test.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/src/stream_chat_configuration.dart'; + +void main() { + group('StreamChatConfigurationProvider', () { + testWidgets( + 'should provide the StreamChatConfiguration class with default data', + (t) async { + final configuration = StreamChatConfigurationData(); + late final StreamChatConfigurationData configurationFromProvider; + await t.pumpWidget(StreamChatConfiguration( + data: configuration, + child: Builder( + builder: (context) { + configurationFromProvider = StreamChatConfiguration.of(context); + return const SizedBox(); + }, + ), + )); + + expect(configuration, configurationFromProvider); + }, + ); + + testWidgets( + 'should provide the StreamChatConfiguration class with custom data', + (t) async { + final configuration = StreamChatConfigurationData().copyWith( + enforceUniqueReactions: false, + ); + late final StreamChatConfigurationData configurationFromProvider; + await t.pumpWidget(StreamChatConfiguration( + data: configuration, + child: Builder( + builder: (context) { + configurationFromProvider = StreamChatConfiguration.of(context); + return const SizedBox(); + }, + ), + )); + + expect(configuration, configurationFromProvider); + }, + ); + }); +} diff --git a/packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart deleted file mode 100644 index 71193da4..00000000 --- a/packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart +++ /dev/null @@ -1,125 +0,0 @@ -// ignore_for_file: deprecated_member_use_from_same_package - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -import '../mocks.dart'; - -void main() { - test('ChannelListViewThemeData copyWith, ==, hashCode basics', () { - expect(const StreamChannelListViewThemeData(), - const StreamChannelListViewThemeData().copyWith()); - }); - - test( - '''Light ChannelListViewThemeData lerps completely to dark ChannelListViewThemeData''', - () { - expect( - const StreamChannelListViewThemeData().lerp( - _channelListViewThemeDataControl, - _channelListViewThemeDataControlDark, - 1), - _channelListViewThemeDataControlDark); - }); - - test( - '''Light ChannelListViewThemeData lerps halfway to dark ChannelListViewThemeData''', - () { - expect( - const StreamChannelListViewThemeData().lerp( - _channelListViewThemeDataControl, - _channelListViewThemeDataControlDark, - 0.5), - _channelListViewThemeDataControlHalfLerp); - }); - - test( - '''Dark ChannelListViewThemeData lerps completely to light ChannelListViewThemeData''', - () { - expect( - const StreamChannelListViewThemeData().lerp( - _channelListViewThemeDataControlDark, - _channelListViewThemeDataControl, - 1), - _channelListViewThemeDataControl); - }); - - test('Merging dark and light themes results in a dark theme', () { - expect( - _channelListViewThemeDataControl - .merge(_channelListViewThemeDataControlDark), - _channelListViewThemeDataControlDark); - }); - - testWidgets( - 'Passing no ChannelListViewThemeData returns default light theme values', - (WidgetTester tester) async { - late BuildContext _context; - await tester.pumpWidget( - MaterialApp( - builder: (context, child) => StreamChat( - client: MockClient(), - child: child, - ), - home: Builder( - builder: (BuildContext context) { - _context = context; - return Scaffold( - body: StreamChannel( - channel: MockChannel(), - child: ChannelListView(), - ), - ); - }, - ), - ), - ); - - final channelListViewTheme = StreamChannelListViewTheme.of(_context); - expect(channelListViewTheme.backgroundColor, - _channelListViewThemeDataControl.backgroundColor); - }); - - testWidgets( - 'Passing no ChannelListViewThemeData returns default dark theme values', - (WidgetTester tester) async { - late BuildContext _context; - await tester.pumpWidget( - MaterialApp( - builder: (context, child) => StreamChat( - client: MockClient(), - streamChatThemeData: StreamChatThemeData.dark(), - child: child, - ), - home: Builder( - builder: (BuildContext context) { - _context = context; - return Scaffold( - body: StreamChannel( - channel: MockChannel(), - child: const StreamMessageListView(), - ), - ); - }, - ), - ), - ); - - final channelListViewTheme = StreamChannelListViewTheme.of(_context); - expect(channelListViewTheme.backgroundColor, - _channelListViewThemeDataControlDark.backgroundColor); - }); -} - -final _channelListViewThemeDataControl = StreamChannelListViewThemeData( - backgroundColor: StreamColorTheme.light().appBg, -); - -const _channelListViewThemeDataControlHalfLerp = StreamChannelListViewThemeData( - backgroundColor: Color(0xff818384), -); - -final _channelListViewThemeDataControlDark = StreamChannelListViewThemeData( - backgroundColor: StreamColorTheme.dark().appBg, -); diff --git a/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart deleted file mode 100644 index a12b52f4..00000000 --- a/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart +++ /dev/null @@ -1,137 +0,0 @@ -// ignore: lines_longer_than_80_chars -// ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -import '../mocks.dart'; - -void main() { - test('MessageSearchListViewThemeData copyWith, ==, hashCode basics', () { - expect(const StreamMessageSearchListViewThemeData(), - const StreamMessageSearchListViewThemeData().copyWith()); - expect(const StreamMessageSearchListViewThemeData().hashCode, - const StreamMessageSearchListViewThemeData().copyWith().hashCode); - }); - - test( - '''Light MessageSearchListViewThemeData lerps completely to dark MessageSearchListViewThemeData''', - () { - expect( - const StreamMessageSearchListViewThemeData().lerp( - _messageSearchListViewThemeDataControl, - _messageSearchListViewThemeDataControlDark, - 1), - _messageSearchListViewThemeDataControlDark); - }); - - test( - '''Light MessageSearchListViewThemeData lerps halfway to dark MessageSearchListViewThemeData''', - () { - expect( - const StreamMessageSearchListViewThemeData().lerp( - _messageSearchListViewThemeDataControl, - _messageSearchListViewThemeDataControlDark, - 0.5), - _messageSearchListViewThemeDataControlHalfLerp); - }); - - test( - '''Dark MessageSearchListViewThemeData lerps completely to light MessageSearchListViewThemeData''', - () { - expect( - const StreamMessageSearchListViewThemeData().lerp( - _messageSearchListViewThemeDataControlDark, - _messageSearchListViewThemeDataControl, - 1), - _messageSearchListViewThemeDataControl); - }); - - test('Merging dark and light themes results in a dark theme', () { - expect( - _messageSearchListViewThemeDataControl - .merge(_messageSearchListViewThemeDataControlDark), - _messageSearchListViewThemeDataControlDark); - }); - - testWidgets( - '''Passing no MessageSearchListViewThemeData returns default light theme values''', - (WidgetTester tester) async { - late BuildContext _context; - await tester.pumpWidget( - MaterialApp( - builder: (context, child) => StreamChat( - client: MockClient(), - child: child, - ), - home: Builder( - builder: (BuildContext context) { - _context = context; - return Scaffold( - body: MessageSearchBloc( - child: MessageSearchListView( - filters: Filter.in_('members', const ['test_id']), - messageQuery: 'test query', - ), - ), - ); - }, - ), - ), - ); - - final messageSearchListViewTheme = - StreamMessageSearchListViewTheme.of(_context); - expect(messageSearchListViewTheme.backgroundColor, - _messageSearchListViewThemeDataControl.backgroundColor); - }); - - testWidgets( - '''Passing no MessageSearchListViewThemeData returns default dark theme values''', - (WidgetTester tester) async { - late BuildContext _context; - await tester.pumpWidget( - MaterialApp( - builder: (context, child) => StreamChat( - client: MockClient(), - streamChatThemeData: StreamChatThemeData.dark(), - child: child, - ), - home: Builder( - builder: (BuildContext context) { - _context = context; - return Scaffold( - body: MessageSearchBloc( - child: MessageSearchListView( - filters: Filter.in_('members', const ['test_id']), - messageQuery: 'test query', - ), - ), - ); - }, - ), - ), - ); - - final messageSearchListViewTheme = - StreamMessageSearchListViewTheme.of(_context); - expect(messageSearchListViewTheme.backgroundColor, - _messageSearchListViewThemeDataControlDark.backgroundColor); - }); -} - -final _messageSearchListViewThemeDataControl = - StreamMessageSearchListViewThemeData( - backgroundColor: StreamColorTheme.light().appBg, -); - -const _messageSearchListViewThemeDataControlHalfLerp = - StreamMessageSearchListViewThemeData( - backgroundColor: Color(0xff818384), -); - -final _messageSearchListViewThemeDataControlDark = - StreamMessageSearchListViewThemeData( - backgroundColor: StreamColorTheme.dark().appBg, -); diff --git a/packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart deleted file mode 100644 index 7b3550c8..00000000 --- a/packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart +++ /dev/null @@ -1,119 +0,0 @@ -// ignore: lines_longer_than_80_chars -// ignore_for_file: deprecated_member_use_from_same_package, deprecated_member_use - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -import '../mocks.dart'; - -void main() { - test('UserListViewThemeData copyWith, ==, hashCode basics', () { - expect(const StreamUserListViewThemeData(), - const StreamUserListViewThemeData().copyWith()); - }); - - test( - '''Light UserListViewThemeData lerps completely to dark UserListViewThemeData''', - () { - expect( - const StreamUserListViewThemeData().lerp(_userListViewThemeDataControl, - _userListViewThemeDataControlDark, 1), - _userListViewThemeDataControlDark); - }); - - test( - '''Light UserListViewThemeData lerps halfway to dark UserListViewThemeData''', - () { - expect( - const StreamUserListViewThemeData().lerp(_userListViewThemeDataControl, - _userListViewThemeDataControlDark, 0.5), - _userListViewThemeDataControlHalfLerp); - }); - - test( - '''Dark UserListViewThemeData lerps completely to light UserListViewThemeData''', - () { - expect( - const StreamUserListViewThemeData().lerp( - _userListViewThemeDataControlDark, - _userListViewThemeDataControl, - 1), - _userListViewThemeDataControl); - }); - - test('Merging dark and light themes results in a dark theme', () { - expect( - _userListViewThemeDataControl.merge(_userListViewThemeDataControlDark), - _userListViewThemeDataControlDark); - }); - - testWidgets( - 'Passing no ChannelListViewThemeData returns default light theme values', - (WidgetTester tester) async { - late BuildContext _context; - await tester.pumpWidget( - MaterialApp( - builder: (context, child) => StreamChat( - client: MockClient(), - child: child, - ), - home: Builder( - builder: (BuildContext context) { - _context = context; - return Scaffold( - body: UsersBloc( - child: UserListView(), - ), - ); - }, - ), - ), - ); - - final userListViewTheme = StreamUserListViewTheme.of(_context); - expect(userListViewTheme.backgroundColor, - _userListViewThemeDataControl.backgroundColor); - }); - - testWidgets( - 'Passing no ChannelListViewThemeData returns default dark theme values', - (WidgetTester tester) async { - late BuildContext _context; - await tester.pumpWidget( - MaterialApp( - builder: (context, child) => StreamChat( - client: MockClient(), - streamChatThemeData: StreamChatThemeData.dark(), - child: child, - ), - home: Builder( - builder: (BuildContext context) { - _context = context; - return Scaffold( - body: UsersBloc( - child: UserListView(), - ), - ); - }, - ), - ), - ); - - final userListViewTheme = StreamUserListViewTheme.of(_context); - expect(userListViewTheme.backgroundColor, - _userListViewThemeDataControlDark.backgroundColor); - }); -} - -final _userListViewThemeDataControl = StreamUserListViewThemeData( - backgroundColor: StreamColorTheme.light().appBg, -); - -const _userListViewThemeDataControlHalfLerp = StreamUserListViewThemeData( - backgroundColor: Color(0xff818384), -); - -final _userListViewThemeDataControlDark = StreamUserListViewThemeData( - backgroundColor: StreamColorTheme.dark().appBg, -); diff --git a/packages/stream_chat_flutter/test/src/extension_test.dart b/packages/stream_chat_flutter/test/src/utils/extension_test.dart similarity index 77% rename from packages/stream_chat_flutter/test/src/extension_test.dart rename to packages/stream_chat_flutter/test/src/utils/extension_test.dart index 696a19fc..1033340c 100644 --- a/packages/stream_chat_flutter/test/src/extension_test.dart +++ b/packages/stream_chat_flutter/test/src/utils/extension_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() { @@ -67,4 +67,21 @@ void main() { expect([user].search('franc'), [user]); }); }); + + group('String.isOnlyEmoji', () { + test('should return false for empty or > 3 strings', () { + expect(''.isOnlyEmoji, false); + expect('aaa📝💜'.isOnlyEmoji, false); + expect('📝💜📝💜'.isOnlyEmoji, false); + }); + + test('should detect strings made only by emojis', () { + expect('a📝💜'.isOnlyEmoji, false); + expect('📝💜📝'.isOnlyEmoji, true); + expect('🌶'.isOnlyEmoji, true); + expect('🌶1'.isOnlyEmoji, false); + expect('👨‍👨👨‍👨'.isOnlyEmoji, true); + expect('👨‍👨👨‍👨 '.isOnlyEmoji, true); + }); + }); } diff --git a/packages/stream_chat_flutter/test/utils/golden.dart b/packages/stream_chat_flutter/test/test_utils/golden.dart similarity index 88% rename from packages/stream_chat_flutter/test/utils/golden.dart rename to packages/stream_chat_flutter/test/test_utils/golden.dart index 436f10f2..7d7a45cc 100644 --- a/packages/stream_chat_flutter/test/utils/golden.dart +++ b/packages/stream_chat_flutter/test/test_utils/golden.dart @@ -1,5 +1,3 @@ -import 'dart:typed_data'; - import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:golden_toolkit/golden_toolkit.dart'; @@ -15,8 +13,7 @@ Future customExpectGoldenMatches( bool? autoHeight, Finder? finder, CustomPump? customPump, - @Deprecated(''' -This method level parameter will be removed in an upcoming release. This can be configured globally. If you have concerns, please file an issue with your use case.''') bool? skip, + bool? skip, }) { final goldenPath = path.join('test/src/goldens'); print('goldenPath: $goldenPath'); diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index eb855ec1..96c2a8ed 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,21 @@ +## 5.0.0 + +- Included the changes from version [4.5.0](#450). + +✅ Added + +- Added `StreamMemberListController`. + +## 5.0.0-beta.2 + +- Included the changes from version [4.4.0](#440) and [4.4.1](#441). + +## 5.0.0-beta.1 + +- Updated `stream_chat` dependency + to [`5.0.0-beta.1`](https://pub.dev/packages/stream_chat/changelog). +- Removed deprecated code. + ## 4.6.0 - Updated `stream_chat` dependency to [`4.6.0`](https://pub.dev/packages/stream_chat/changelog). @@ -5,19 +23,11 @@ ## 4.5.0 - Updated `stream_chat` dependency to [`4.5.0`](https://pub.dev/packages/stream_chat/changelog). - -🐞 Fixed - - [#1269](https://github.com/GetStream/stream-chat-flutter/issues/1269) Fix `ChannelListEventHandler` castError at PagedValue.asSuccess. - [#1241](https://github.com/GetStream/stream-chat-flutter/issues/1241) StreamChannelListView load more indicator non stop. -✅ Added - -- Added setters to update `ChannelListController.filter` and `ChannelListController.sort` properties - in order to support runtime changes. - ## 4.4.1 - Updated `stream_chat` dependency to [`4.4.1`](https://pub.dev/packages/stream_chat/changelog). diff --git a/packages/stream_chat_flutter_core/example/lib/main.dart b/packages/stream_chat_flutter_core/example/lib/main.dart index f77dc2d5..0a523f81 100644 --- a/packages/stream_chat_flutter_core/example/lib/main.dart +++ b/packages/stream_chat_flutter_core/example/lib/main.dart @@ -319,8 +319,7 @@ class _MessageScreenState extends State { children: [ Expanded( child: TextField( - controller: messageInputController.textEditingController, - onChanged: (s) => messageInputController.text = s, + controller: messageInputController.textFieldController, decoration: const InputDecoration( hintText: 'Enter your message', ), @@ -332,8 +331,7 @@ class _MessageScreenState extends State { clipBehavior: Clip.hardEdge, child: InkWell( onTap: () async { - if (messageInputController.message.text?.isNotEmpty == - true) { + if (messageInputController.text.isNotEmpty) { await channel.sendMessage( messageInputController.message, ); diff --git a/packages/stream_chat_flutter_core/example/pubspec.yaml b/packages/stream_chat_flutter_core/example/pubspec.yaml index 4ce0c7e9..6b822c4e 100644 --- a/packages/stream_chat_flutter_core/example/pubspec.yaml +++ b/packages/stream_chat_flutter_core/example/pubspec.yaml @@ -26,7 +26,7 @@ dependencies: cupertino_icons: ^1.0.3 flutter: sdk: flutter - stream_chat_flutter_core: ^4.6.0 + stream_chat_flutter_core: ^5.0.0-beta.2 dev_dependencies: flutter_test: sdk: flutter diff --git a/packages/stream_chat_flutter_core/example/windows/flutter/generated_plugins.cmake b/packages/stream_chat_flutter_core/example/windows/flutter/generated_plugins.cmake index ba4a2175..8cf5d426 100644 --- a/packages/stream_chat_flutter_core/example/windows/flutter/generated_plugins.cmake +++ b/packages/stream_chat_flutter_core/example/windows/flutter/generated_plugins.cmake @@ -6,6 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST connectivity_plus_windows ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -14,3 +17,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart b/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart deleted file mode 100644 index 556b44cc..00000000 --- a/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart +++ /dev/null @@ -1,261 +0,0 @@ -// ignore_for_file: deprecated_member_use_from_same_package - -import 'dart:async'; -import 'dart:convert'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_flutter_core/src/better_stream_builder.dart'; -import 'package:stream_chat_flutter_core/src/channels_bloc.dart'; -import 'package:stream_chat_flutter_core/src/stream_chat_core.dart'; -import 'package:stream_chat_flutter_core/src/typedef.dart'; - -/// [ChannelListCore] is a simplified class that allows fetching a list of -/// channels while exposing UI builders. -/// A [ChannelListController] is used to reload and paginate data. -/// -/// -/// ```dart -/// class ChannelListPage extends StatelessWidget { -/// @override -/// Widget build(BuildContext context) { -/// return Scaffold( -/// body: ChannelListCore( -/// filter: Filter.in_( -/// 'members', -/// [StreamChat.of(context).user!.id], -/// ), -/// sort: [SortOption('last_message_at')], -/// pagination: PaginationParams( -/// limit: 20, -/// ), -/// errorBuilder: (context, err) { -/// return Center( -/// child: Text('An error has occured'), -/// ); -/// }, -/// emptyBuilder: (context) { -/// return Center( -/// child: Text('Nothing here...'), -/// ); -/// }, -/// loadingBuilder: (context) { -/// return Center( -/// child: CircularProgressIndicator(), -/// ); -/// }, -/// listBuilder: (context, list) { -/// return ChannelPage(list); -/// } -/// ), -/// ); -/// } -/// } -/// ``` -/// -/// Make sure to have a [StreamChatCore] ancestor in order to provide the -/// information about the channels. -@Deprecated(''' -ChannelListCore is deprecated and will be removed in the next -major version. Use StreamChannelListController instead to create your custom list. -More details here https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter_core/stream_channel_list_controller -''') -class ChannelListCore extends StatefulWidget { - /// Instantiate a new ChannelListView - const ChannelListCore({ - super.key, - required this.errorBuilder, - required this.emptyBuilder, - required this.loadingBuilder, - required this.listBuilder, - this.filter, - this.state = true, - this.watch = true, - this.presence = false, - this.memberLimit, - this.messageLimit, - this.sort, - this.channelListController, - this.limit = 25, - }); - - /// A [ChannelListController] allows reloading and pagination. - /// Use [ChannelListController.loadData] and - /// [ChannelListController.paginateData] respectively for reloading and - /// pagination. - final ChannelListController? channelListController; - - /// The builder that will be used in case of error - final ErrorBuilder errorBuilder; - - /// The builder that will be used in case of loading - final WidgetBuilder loadingBuilder; - - /// The builder which is used when list of channels loads - final Function(BuildContext, List) listBuilder; - - /// The builder used when the channel list is empty. - final WidgetBuilder emptyBuilder; - - /// The query filters to use. - /// You can query on any of the custom fields you've defined on the [Channel]. - /// You can also filter other built-in channel fields. - final Filter? filter; - - /// The sorting used for the channels matching the filters. - /// Sorting is based on field and direction, multiple sorting options can be - /// provided. - /// You can sort based on last_updated, last_message_at, updated_at, created - /// _at or member_count. Direction can be ascending or descending. - final List>? sort; - - /// If true returns the Channel state - final bool state; - - /// If true listen to changes to this Channel in real time. - final bool watch; - - /// If true you’ll receive user presence updates via the websocket events - final bool presence; - - /// Number of members to fetch in each channel - final int? memberLimit; - - /// Number of messages to fetch in each channel - final int? messageLimit; - - /// The amount of channels requested per API call. - final int limit; - - @override - ChannelListCoreState createState() => ChannelListCoreState(); -} - -/// The current state of the [ChannelListCore]. -class ChannelListCoreState extends State { - late ChannelsBlocState _channelsBloc; - StreamChatCoreState? _streamChatCoreState; - - @override - Widget build(BuildContext context) => _buildListView(_channelsBloc); - - BetterStreamBuilder> _buildListView( - ChannelsBlocState channelsBlocState, - ) => - BetterStreamBuilder>( - stream: channelsBlocState.channelsStream, - errorBuilder: widget.errorBuilder, - noDataBuilder: widget.loadingBuilder, - builder: (context, channels) { - if (channels.isEmpty) { - return widget.emptyBuilder(context); - } - return widget.listBuilder(context, channels); - }, - ); - - /// Fetches initial channels and updates the widget - Future loadData() => _channelsBloc.queryChannels( - filter: widget.filter, - sortOptions: widget.sort, - state: widget.state, - watch: widget.watch, - presence: widget.presence, - memberLimit: widget.memberLimit, - messageLimit: widget.messageLimit, - paginationParams: PaginationParams(limit: widget.limit, offset: 0), - ); - - /// Fetches more channels with updated pagination and updates the widget - Future paginateData() => _channelsBloc.queryChannels( - filter: widget.filter, - sortOptions: widget.sort, - state: widget.state, - watch: widget.watch, - presence: widget.presence, - memberLimit: widget.memberLimit, - messageLimit: widget.messageLimit, - paginationParams: PaginationParams( - limit: widget.limit, - offset: _channelsBloc.channels?.length ?? 0, - ), - ); - - StreamSubscription? _subscription; - - @override - void initState() { - super.initState(); - _setupController(); - } - - @override - void didChangeDependencies() { - _channelsBloc = ChannelsBloc.of(context); - final newStreamChatCoreState = StreamChatCore.of(context); - - if (newStreamChatCoreState != _streamChatCoreState) { - _streamChatCoreState = newStreamChatCoreState; - loadData(); - final client = _streamChatCoreState!.client; - _subscription?.cancel(); - _subscription = client - .on( - EventType.connectionRecovered, - EventType.notificationAddedToChannel, - EventType.notificationMessageNew, - EventType.channelVisible, - ) - .listen((event) => loadData()); - } - - super.didChangeDependencies(); - } - - @override - void didUpdateWidget(ChannelListCore oldWidget) { - super.didUpdateWidget(oldWidget); - - if (jsonEncode(widget.filter) != jsonEncode(oldWidget.filter) || - jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) || - widget.state != oldWidget.state || - widget.watch != oldWidget.watch || - widget.presence != oldWidget.presence || - widget.messageLimit != oldWidget.messageLimit || - widget.memberLimit != oldWidget.memberLimit || - widget.limit != oldWidget.limit) { - loadData(); - } - - if (widget.channelListController != oldWidget.channelListController) { - _setupController(); - } - } - - void _setupController() { - if (widget.channelListController != null) { - widget.channelListController!.loadData = loadData; - widget.channelListController!.paginateData = paginateData; - } - } - - @override - void dispose() { - _subscription?.cancel(); - super.dispose(); - } -} - -/// Controller used for loading more data and controlling pagination in -/// [ChannelListCore]. -class ChannelListController { - /// This function calls Stream's servers to load a list of channels. - /// If there is existing data, calling this function causes a reload. - AsyncCallback? loadData; - - /// This function is used to load another page of data. Note, [loadData] - /// should be used to populate the initial page of data. Calling - /// [paginateData] performs a query to load subsequent pages. - AsyncCallback? paginateData; -} diff --git a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart deleted file mode 100644 index df791d52..00000000 --- a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart +++ /dev/null @@ -1,253 +0,0 @@ -// ignore_for_file: deprecated_member_use_from_same_package - -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:rxdart/rxdart.dart'; -import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_flutter_core/src/channel_list_core.dart'; -import 'package:stream_chat_flutter_core/src/stream_chat_core.dart'; -import 'package:stream_chat_flutter_core/src/stream_controller_extension.dart'; - -/// Widget dedicated to the management of a channel list with pagination -/// [ChannelsBloc] is used together with [ChannelListCore] to manage a list of -/// [Channel]s with pagination, re-ordering, querying and other operations -/// associated with [Channel]s. -/// -/// [ChannelsBloc] can be access at anytime by using the static [of] method -/// using Flutter's [BuildContext]. -/// -/// API docs: https://getstream.io/chat/docs/flutter-dart/query_channels/ -@Deprecated("Use 'StreamChannelListController' instead") -class ChannelsBloc extends StatefulWidget { - /// Creates a new [ChannelsBloc]. The parameter [child] must be supplied and - /// not null. - const ChannelsBloc({ - super.key, - required this.child, - this.lockChannelsOrder = false, - this.channelsComparator, - this.shouldAddChannel, - }); - - /// The widget child - final Widget child; - - /// Set this to true to prevent channels to be brought to the top of the list - /// when a new message arrives - final bool lockChannelsOrder; - - /// Comparator used to sort the channels when a message.new event is received - final Comparator? channelsComparator; - - /// Function used to evaluate if a channel should be added to the list when a - /// message.new event is received - final bool Function(Event)? shouldAddChannel; - - @override - ChannelsBlocState createState() => ChannelsBlocState(); - - /// Use this method to get the current [ChannelsBlocState] instance - static ChannelsBlocState of(BuildContext context) { - ChannelsBlocState? streamChatState; - - streamChatState = context.findAncestorStateOfType(); - - assert( - streamChatState != null, - 'You must have a ChannelsBloc widget as ancestor', - ); - - return streamChatState!; - } -} - -/// The current state of the [ChannelsBloc]. -class ChannelsBlocState extends State - with AutomaticKeepAliveClientMixin { - StreamChatCoreState? _streamChatCoreState; - - @override - Widget build(BuildContext context) { - super.build(context); - return widget.child; - } - - /// The current channel list - List? get channels => _channelsController.valueOrNull; - - /// The current channel list as a stream - Stream> get channelsStream => _channelsController.stream; - - final _queryChannelsLoadingController = BehaviorSubject.seeded(false); - - final BehaviorSubject> _channelsController = - BehaviorSubject>(); - - /// The stream notifying the state of queryChannel call - Stream get queryChannelsLoading => - _queryChannelsLoadingController.stream; - - final List _hiddenChannels = []; - - bool _paginationEnded = false; - - final List _subscriptions = []; - - /// Calls [client.queryChannels] updating [queryChannelsLoading] stream - Future queryChannels({ - Filter? filter, - List>? sortOptions, - bool state = true, - bool watch = true, - bool presence = false, - int? memberLimit, - int? messageLimit, - bool waitForConnect = true, - PaginationParams paginationParams = const PaginationParams(limit: 30), - }) async { - final client = _streamChatCoreState!.client; - - final offset = paginationParams.offset; - final clear = offset == null || offset == 0; - if (clear && _paginationEnded) { - _paginationEnded = false; - } - - if ((!clear && _paginationEnded) || _queryChannelsLoadingController.value) { - return; - } - - if (_channelsController.hasValue) { - _queryChannelsLoadingController.safeAdd(true); - } - - try { - final oldChannels = List.from(channels ?? []); - var newChannels = []; - await for (final channels in client.queryChannels( - filter: filter, - sort: sortOptions, - state: state, - watch: watch, - presence: presence, - memberLimit: memberLimit, - messageLimit: messageLimit, - waitForConnect: waitForConnect, - paginationParams: paginationParams, - )) { - newChannels = channels; - if (clear) { - _channelsController.safeAdd(channels); - } else { - final temp = oldChannels + channels; - _channelsController.safeAdd(temp); - } - if (_channelsController.hasValue && - _queryChannelsLoadingController.value) { - _queryChannelsLoadingController.safeAdd(false); - } - } - if (newChannels.isEmpty || newChannels.length < paginationParams.limit) { - _paginationEnded = true; - } - } catch (e, stk) { - // reset loading controller - _queryChannelsLoadingController.safeAdd(false); - if (_channelsController.hasValue) { - _queryChannelsLoadingController.safeAddError(e, stk); - } else { - _channelsController.safeAddError(e, stk); - } - } - } - - @override - void didChangeDependencies() { - final newStreamChatCoreState = StreamChatCore.of(context); - - if (newStreamChatCoreState != _streamChatCoreState) { - _streamChatCoreState = newStreamChatCoreState; - final client = _streamChatCoreState!.client; - - _cancelSubscriptions(); - if (!widget.lockChannelsOrder) { - _subscriptions.add(client - .on( - EventType.messageNew, - ) - .listen((e) { - if (e.message?.parentId != null && e.message?.showInChannel != true) { - return; - } - final newChannels = List.from(channels ?? []); - final index = newChannels.indexWhere((c) => c.cid == e.cid); - if (index != -1) { - if (index > 0) { - final channel = newChannels.removeAt(index); - newChannels.insert(0, channel); - } - } else if (widget.shouldAddChannel?.call(e) == true) { - final hiddenIndex = - _hiddenChannels.indexWhere((c) => c.cid == e.cid); - if (hiddenIndex != -1) { - newChannels.insert(0, _hiddenChannels[hiddenIndex]); - _hiddenChannels.removeAt(hiddenIndex); - } else { - if (client.state.channels[e.cid] != null) { - newChannels.insert(0, client.state.channels[e.cid]!); - } - } - } - - if (widget.channelsComparator != null) { - newChannels.sort(widget.channelsComparator); - } - _channelsController.safeAdd(newChannels); - })); - } - - _subscriptions - ..add(client.on(EventType.channelHidden).listen((event) async { - final newChannels = List.from(channels ?? []); - final channelIndex = - newChannels.indexWhere((c) => c.cid == event.cid); - if (channelIndex > -1) { - final channel = newChannels.removeAt(channelIndex); - _hiddenChannels.add(channel); - _channelsController.safeAdd(newChannels); - } - })) - ..add(client - .on( - EventType.channelDeleted, - EventType.notificationRemovedFromChannel, - ) - .listen((e) { - final channel = e.channel; - _channelsController.safeAdd(List.from( - (channels ?? [])..removeWhere((c) => c.cid == channel?.cid), - )); - })); - } - - super.didChangeDependencies(); - } - - @override - void dispose() { - _channelsController.close(); - _queryChannelsLoadingController.close(); - _cancelSubscriptions(); - super.dispose(); - } - - void _cancelSubscriptions() { - _subscriptions - ..forEach((s) => s.cancel()) - ..clear(); - } - - @override - bool get wantKeepAlive => true; -} diff --git a/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart b/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart deleted file mode 100644 index 0ba47fd2..00000000 --- a/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart +++ /dev/null @@ -1,170 +0,0 @@ -// ignore_for_file: deprecated_member_use_from_same_package - -import 'package:flutter/material.dart'; -import 'package:rxdart/rxdart.dart'; -import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_flutter_core/src/stream_chat_core.dart'; -import 'package:stream_chat_flutter_core/src/stream_controller_extension.dart'; - -/// [MessageSearchBloc] is used to manage a list of messages with pagination. -/// This class can be used to load messages, perform queries, etc. -/// -/// [MessageSearchBloc] can be access at anytime by using the static [of] method -/// using Flutter's [BuildContext]. -/// -/// API docs: https://getstream.io/chat/docs/flutter-dart/send_message/ -@Deprecated("Use 'StreamMessageSearchListController' instead") -class MessageSearchBloc extends StatefulWidget { - /// Instantiate a new MessageSearchBloc - const MessageSearchBloc({ - super.key, - required this.child, - }); - - /// The widget child - final Widget child; - - @override - MessageSearchBlocState createState() => MessageSearchBlocState(); - - /// Use this method to get the current [MessageSearchBlocState] instance - static MessageSearchBlocState of(BuildContext context) { - MessageSearchBlocState? state; - - state = context.findAncestorStateOfType(); - - assert( - state != null, - 'You must have a MessageSearchBloc widget as ancestor', - ); - - return state!; - } -} - -/// The current state of the [MessageSearchBloc] -class MessageSearchBlocState extends State - with AutomaticKeepAliveClientMixin { - late StreamChatCoreState _streamChatCoreState; - - /// The key used to paginate next items. - String? nextId; - - /// The key used to paginate previous items. - String? previousId; - - /// The current messages list - List? get messageResponses => - _messageResponses.valueOrNull; - - /// The current messages list as a stream - Stream> get messagesStream => - _messageResponses.stream; - - final _messageResponses = BehaviorSubject>(); - - final _queryMessagesLoadingController = BehaviorSubject.seeded(false); - - /// The stream notifying the state of queryUsers call - Stream get queryMessagesLoading => - _queryMessagesLoadingController.stream; - - bool _paginationEnded = false; - - /// Calls [StreamChatClient.search] updating - /// [messagesStream] and [queryMessagesLoading] stream - Future search({ - required Filter filter, - Filter? messageFilter, - List? sort, - String? query, - PaginationParams pagination = const PaginationParams(limit: 30), - }) async { - final client = _streamChatCoreState.client; - - var clear = false; - if (sort != null) { - clear |= pagination.next == null; - } else { - final offset = pagination.offset; - clear |= offset == null || offset == 0; - } - - if (clear && _paginationEnded) { - _paginationEnded = false; - } - - if ((!clear && _paginationEnded) || _queryMessagesLoadingController.value) { - return; - } - - if (_messageResponses.hasValue) { - _queryMessagesLoadingController.safeAdd(true); - } - try { - final oldMessages = List.from(messageResponses ?? []); - - final response = await client.search( - filter, - sort: sort, - query: query, - paginationParams: pagination, - messageFilters: messageFilter, - ); - - final next = response.next; - final previous = response.previous; - - nextId = next != null && next.isNotEmpty - ? next - : /*reset nextId if we get nothing*/ null; - previousId = previous != null && previous.isNotEmpty - ? previous - : /*reset previousId if we get nothing*/ null; - - final newMessages = response.results; - if (clear) { - _messageResponses.safeAdd(newMessages); - } else { - final temp = oldMessages + newMessages; - _messageResponses.safeAdd(temp); - } - if (_messageResponses.hasValue && _queryMessagesLoadingController.value) { - _queryMessagesLoadingController.safeAdd(false); - } - if (newMessages.isEmpty || newMessages.length < pagination.limit) { - _paginationEnded = true; - } - } catch (e, stk) { - // reset loading controller - _queryMessagesLoadingController.safeAdd(false); - if (_messageResponses.hasValue) { - _queryMessagesLoadingController.safeAddError(e, stk); - } else { - _messageResponses.safeAddError(e, stk); - } - } - } - - @override - Widget build(BuildContext context) { - super.build(context); - return widget.child; - } - - @override - void didChangeDependencies() { - _streamChatCoreState = StreamChatCore.of(context); - super.didChangeDependencies(); - } - - @override - void dispose() { - _messageResponses.close(); - _queryMessagesLoadingController.close(); - super.dispose(); - } - - @override - bool get wantKeepAlive => true; -} diff --git a/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart b/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart deleted file mode 100644 index f528a4da..00000000 --- a/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart +++ /dev/null @@ -1,236 +0,0 @@ -// ignore_for_file: deprecated_member_use_from_same_package - -import 'dart:convert'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_flutter_core/src/better_stream_builder.dart'; -import 'package:stream_chat_flutter_core/src/message_search_bloc.dart'; -import 'package:stream_chat_flutter_core/src/typedef.dart'; - -/// -/// [MessageSearchListCore] is a simplified class that allows searching for -/// messages across channels while exposing UI builders. -/// A [MessageSearchListController] is used to load and paginate data. -/// -/// ```dart -/// class MessageSearchPage extends StatelessWidget { -/// @override -/// Widget build(BuildContext context) { -/// return Scaffold( -/// body: MessageSearchListCore( -/// messageQuery: _messageFilter, -/// filters: _channelsFilter, -/// limit: 20, -/// ), -/// ); -/// } -/// } -/// ``` -/// -/// Make sure to have a [MessageSearchBloc] ancestor in order to provide the -/// information about the messages. -/// The widget uses a [ListView.separated] to render the list of messages. -/// -@Deprecated(''' -MessageSearchListCore is deprecated and will be removed in the next -major version. Use StreamMessageSearchListController instead to create your custom list. -More details here https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter_core/stream_message_search_list_controller -''') -class MessageSearchListCore extends StatefulWidget { - /// Instantiate a new [MessageSearchListView]. - /// The following parameters must be supplied and not null: - /// * [emptyBuilder] - /// * [errorBuilder] - /// * [loadingBuilder] - /// * [childBuilder] - MessageSearchListCore({ - super.key, - required this.emptyBuilder, - required this.errorBuilder, - required this.loadingBuilder, - required this.childBuilder, - required this.filters, - this.messageQuery, - this.sortOptions, - @Deprecated( - "'pagination' is deprecated and shouldn't be used. " - "This property is no longer used, Please use 'limit' instead", - ) - this.paginationParams, - this.messageFilters, - this.messageSearchListController, - int? limit, - }) : assert( - messageQuery != null || messageFilters != null, - 'Provide at least `query` or `messageFilters`', - ), - assert( - messageQuery == null || messageFilters == null, - "Can't provide both `query` and `messageFilters` at the same time", - ), - assert( - paginationParams?.offset == null || - paginationParams?.offset == 0 || - sortOptions == null, - 'Cannot specify `offset` with `sortOptions` parameter', - ), - limit = limit ?? paginationParams?.limit ?? 30; - - /// A [MessageSearchListController] allows reloading and pagination. - /// Use [MessageSearchListController.loadData] and - /// [MessageSearchListController.paginateData] respectively for reloading and - /// pagination. - final MessageSearchListController? messageSearchListController; - - /// Message String to search on - final String? messageQuery; - - /// The query filters to use. - /// You can query on any of the custom fields you've defined on the [Channel]. - /// You can also filter other built-in channel fields. - final Filter filters; - - /// The sorting used for the channels matching the filters. - /// Sorting is based on field and direction, multiple sorting options can be - /// provided. - /// You can sort based on last_updated, last_message_at, updated_at, created_ - /// at or member_count. Direction can be ascending or descending. - final List? sortOptions; - - /// Pagination parameters - /// limit: the number of messages to return (max is 30) - /// offset: the offset (max is 1000) - @Deprecated( - "'pagination' is deprecated and shouldn't be used. " - "This property is no longer used, Please use 'limit' instead", - ) - final PaginationParams? paginationParams; - - /// The amount of messages requested per API call. - final int limit; - - /// The message query filters to use. - /// You can query on any of the custom fields you've defined on the [Channel]. - /// You can also filter other built-in channel fields. - final Filter? messageFilters; - - /// The builder that is used when the search messages are fetched - final Widget Function(List) childBuilder; - - /// The builder used when the channel list is empty. - final WidgetBuilder emptyBuilder; - - /// The builder that will be used in case of error - final ErrorBuilder errorBuilder; - - /// The builder that will be used in case of loading - final WidgetBuilder loadingBuilder; - - @override - MessageSearchListCoreState createState() => MessageSearchListCoreState(); -} - -/// The current state of the [MessageSearchListCore]. -class MessageSearchListCoreState extends State { - MessageSearchBlocState? _messageSearchBloc; - - @override - void didChangeDependencies() { - final newMessageSearchBloc = MessageSearchBloc.of(context); - - if (newMessageSearchBloc != _messageSearchBloc) { - _messageSearchBloc = newMessageSearchBloc; - loadData(); - } - - super.didChangeDependencies(); - } - - void _setupController() { - if (widget.messageSearchListController != null) { - widget.messageSearchListController!.loadData = loadData; - widget.messageSearchListController!.paginateData = paginateData; - } - } - - @override - void initState() { - super.initState(); - _setupController(); - } - - @override - Widget build(BuildContext context) => _buildListView(_messageSearchBloc!); - - Widget _buildListView(MessageSearchBlocState messageSearchBloc) => - BetterStreamBuilder>( - stream: messageSearchBloc.messagesStream, - errorBuilder: widget.errorBuilder, - noDataBuilder: widget.loadingBuilder, - builder: (context, items) { - if (items.isEmpty) { - return widget.emptyBuilder(context); - } - return widget.childBuilder(items); - }, - ); - - /// Fetches initial messages and updates the widget - Future loadData() => _messageSearchBloc!.search( - filter: widget.filters, - sort: widget.sortOptions, - query: widget.messageQuery, - messageFilter: widget.messageFilters, - pagination: PaginationParams(limit: widget.limit), - ); - - /// Fetches more messages with updated pagination and updates the widget - Future paginateData() { - var pagination = PaginationParams(limit: widget.limit); - if (widget.sortOptions != null) { - pagination = pagination.copyWith( - next: _messageSearchBloc?.nextId, - ); - } else { - pagination = pagination.copyWith( - offset: _messageSearchBloc?.messageResponses?.length, - ); - } - return _messageSearchBloc!.search( - filter: widget.filters, - sort: widget.sortOptions, - pagination: pagination, - query: widget.messageQuery, - messageFilter: widget.messageFilters, - ); - } - - @override - void didUpdateWidget(MessageSearchListCore oldWidget) { - super.didUpdateWidget(oldWidget); - if (jsonEncode(widget.filters) != jsonEncode(oldWidget.filters) || - jsonEncode(widget.sortOptions) != jsonEncode(oldWidget.sortOptions) || - widget.messageQuery != oldWidget.messageQuery || - jsonEncode(widget.messageFilters) != - jsonEncode(oldWidget.messageFilters) || - widget.limit != oldWidget.limit) { - loadData(); - } - - if (widget.messageSearchListController != - oldWidget.messageSearchListController) { - _setupController(); - } - } -} - -/// Controller used for paginating data in [ChannelListView] -class MessageSearchListController { - /// Call this function to reload data - AsyncCallback? loadData; - - /// Call this function to load further data - AsyncCallback? paginateData; -} diff --git a/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.freezed.dart b/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.freezed.dart index a7ea0ea3..9e325531 100644 --- a/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.freezed.dart +++ b/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.freezed.dart @@ -1,5 +1,6 @@ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target part of 'paged_value_notifier.dart'; @@ -11,34 +12,7 @@ part of 'paged_value_notifier.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); - -/// @nodoc -class _$PagedValueTearOff { - const _$PagedValueTearOff(); - - Success call( - {required List items, Key? nextPageKey, StreamChatError? error}) { - return Success( - items: items, - nextPageKey: nextPageKey, - error: error, - ); - } - - Loading loading() { - return Loading(); - } - - Error error(StreamChatError error) { - return Error( - error, - ); - } -} - -/// @nodoc -const $PagedValue = _$PagedValueTearOff(); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); /// @nodoc mixin _$PagedValue { @@ -112,23 +86,23 @@ class _$PagedValueCopyWithImpl } /// @nodoc -abstract class $SuccessCopyWith { - factory $SuccessCopyWith( - Success value, $Res Function(Success) then) = - _$SuccessCopyWithImpl; +abstract class _$$SuccessCopyWith { + factory _$$SuccessCopyWith(_$Success value, + $Res Function(_$Success) then) = + __$$SuccessCopyWithImpl; $Res call({List items, Key? nextPageKey, StreamChatError? error}); } /// @nodoc -class _$SuccessCopyWithImpl +class __$$SuccessCopyWithImpl extends _$PagedValueCopyWithImpl - implements $SuccessCopyWith { - _$SuccessCopyWithImpl( - Success _value, $Res Function(Success) _then) - : super(_value, (v) => _then(v as Success)); + implements _$$SuccessCopyWith { + __$$SuccessCopyWithImpl( + _$Success _value, $Res Function(_$Success) _then) + : super(_value, (v) => _then(v as _$Success)); @override - Success get _value => super._value as Success; + _$Success get _value => super._value as _$Success; @override $Res call({ @@ -136,9 +110,9 @@ class _$SuccessCopyWithImpl Object? nextPageKey = freezed, Object? error = freezed, }) { - return _then(Success( + return _then(_$Success( items: items == freezed - ? _value.items + ? _value._items : items // ignore: cast_nullable_to_non_nullable as List, nextPageKey: nextPageKey == freezed @@ -157,20 +131,27 @@ class _$SuccessCopyWithImpl class _$Success extends Success with DiagnosticableTreeMixin { - const _$Success({required this.items, this.nextPageKey, this.error}) - : super._(); - - @override + const _$Success( + {required final List items, this.nextPageKey, this.error}) + : _items = items, + super._(); + + /// List with all items loaded so far. + final List _items; /// List with all items loaded so far. - final List items; @override + List get items { + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_items); + } /// The key for the next page to be fetched. - final Key? nextPageKey; @override + final Key? nextPageKey; /// The current error, if any. + @override final StreamChatError? error; @override @@ -192,8 +173,8 @@ class _$Success extends Success bool operator ==(dynamic other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is Success && - const DeepCollectionEquality().equals(other.items, items) && + other is _$Success && + const DeepCollectionEquality().equals(other._items, _items) && const DeepCollectionEquality() .equals(other.nextPageKey, nextPageKey) && const DeepCollectionEquality().equals(other.error, error)); @@ -202,14 +183,15 @@ class _$Success extends Success @override int get hashCode => Object.hash( runtimeType, - const DeepCollectionEquality().hash(items), + const DeepCollectionEquality().hash(_items), const DeepCollectionEquality().hash(nextPageKey), const DeepCollectionEquality().hash(error)); @JsonKey(ignore: true) @override - $SuccessCopyWith> get copyWith => - _$SuccessCopyWithImpl>(this, _$identity); + _$$SuccessCopyWith> get copyWith => + __$$SuccessCopyWithImpl>( + this, _$identity); @override @optionalTypeArgs @@ -288,41 +270,41 @@ class _$Success extends Success abstract class Success extends PagedValue { const factory Success( - {required List items, - Key? nextPageKey, - StreamChatError? error}) = _$Success; + {required final List items, + final Key? nextPageKey, + final StreamChatError? error}) = _$Success; const Success._() : super._(); /// List with all items loaded so far. - List get items; + List get items => throw _privateConstructorUsedError; /// The key for the next page to be fetched. - Key? get nextPageKey; + Key? get nextPageKey => throw _privateConstructorUsedError; /// The current error, if any. - StreamChatError? get error; + StreamChatError? get error => throw _privateConstructorUsedError; @JsonKey(ignore: true) - $SuccessCopyWith> get copyWith => + _$$SuccessCopyWith> get copyWith => throw _privateConstructorUsedError; } /// @nodoc -abstract class $LoadingCopyWith { - factory $LoadingCopyWith( - Loading value, $Res Function(Loading) then) = - _$LoadingCopyWithImpl; +abstract class _$$LoadingCopyWith { + factory _$$LoadingCopyWith(_$Loading value, + $Res Function(_$Loading) then) = + __$$LoadingCopyWithImpl; } /// @nodoc -class _$LoadingCopyWithImpl +class __$$LoadingCopyWithImpl extends _$PagedValueCopyWithImpl - implements $LoadingCopyWith { - _$LoadingCopyWithImpl( - Loading _value, $Res Function(Loading) _then) - : super(_value, (v) => _then(v as Loading)); + implements _$$LoadingCopyWith { + __$$LoadingCopyWithImpl( + _$Loading _value, $Res Function(_$Loading) _then) + : super(_value, (v) => _then(v as _$Loading)); @override - Loading get _value => super._value as Loading; + _$Loading get _value => super._value as _$Loading; } /// @nodoc @@ -340,13 +322,13 @@ class _$Loading extends Loading void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties - ..add(DiagnosticsProperty('type', 'PagedValue<$Key, $Value>.loading')); + .add(DiagnosticsProperty('type', 'PagedValue<$Key, $Value>.loading')); } @override bool operator ==(dynamic other) { return identical(this, other) || - (other.runtimeType == runtimeType && other is Loading); + (other.runtimeType == runtimeType && other is _$Loading); } @override @@ -433,29 +415,29 @@ abstract class Loading extends PagedValue { } /// @nodoc -abstract class $ErrorCopyWith { - factory $ErrorCopyWith( - Error value, $Res Function(Error) then) = - _$ErrorCopyWithImpl; +abstract class _$$ErrorCopyWith { + factory _$$ErrorCopyWith( + _$Error value, $Res Function(_$Error) then) = + __$$ErrorCopyWithImpl; $Res call({StreamChatError error}); } /// @nodoc -class _$ErrorCopyWithImpl +class __$$ErrorCopyWithImpl extends _$PagedValueCopyWithImpl - implements $ErrorCopyWith { - _$ErrorCopyWithImpl( - Error _value, $Res Function(Error) _then) - : super(_value, (v) => _then(v as Error)); + implements _$$ErrorCopyWith { + __$$ErrorCopyWithImpl( + _$Error _value, $Res Function(_$Error) _then) + : super(_value, (v) => _then(v as _$Error)); @override - Error get _value => super._value as Error; + _$Error get _value => super._value as _$Error; @override $Res call({ Object? error = freezed, }) { - return _then(Error( + return _then(_$Error( error == freezed ? _value.error : error // ignore: cast_nullable_to_non_nullable @@ -490,7 +472,7 @@ class _$Error extends Error bool operator ==(dynamic other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is Error && + other is _$Error && const DeepCollectionEquality().equals(other.error, error)); } @@ -500,8 +482,8 @@ class _$Error extends Error @JsonKey(ignore: true) @override - $ErrorCopyWith> get copyWith => - _$ErrorCopyWithImpl>(this, _$identity); + _$$ErrorCopyWith> get copyWith => + __$$ErrorCopyWithImpl>(this, _$identity); @override @optionalTypeArgs @@ -579,11 +561,11 @@ class _$Error extends Error } abstract class Error extends PagedValue { - const factory Error(StreamChatError error) = _$Error; + const factory Error(final StreamChatError error) = _$Error; const Error._() : super._(); - StreamChatError get error; + StreamChatError get error => throw _privateConstructorUsedError; @JsonKey(ignore: true) - $ErrorCopyWith> get copyWith => + _$$ErrorCopyWith> get copyWith => throw _privateConstructorUsedError; } diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart index 18e494e2..bf37aa11 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart @@ -19,7 +19,7 @@ enum QueryDirection { /// /// Use [StreamChannel.of] to get the current [StreamChannelState] instance. class StreamChannel extends StatefulWidget { - /// Creates a new instance of [StreamChannel]. Both [child] and [client] must + /// Creates a new instance of [StreamChannel]. Both [child] and [channel] must /// be supplied and not null. const StreamChannel({ super.key, @@ -220,8 +220,6 @@ class StreamChannelState extends State { /// Loads channel at specific message Future loadChannelAtMessage( String? messageId, { - @Deprecated('before is deprecated, use limit instead') int before = 20, - @Deprecated('after is deprecated, use limit instead') int after = 20, int limit = 20, bool preferOffline = false, }) => @@ -231,9 +229,21 @@ class StreamChannelState extends State { preferOffline: preferOffline, ); + /// Loads channel at specific message + Future loadChannelAtTimestamp( + DateTime timestamp, { + int limit = 40, + bool preferOffline = false, + }) => + _queryAtTimestamp( + timestamp: timestamp, + limit: limit, + preferOffline: preferOffline, + ); + Future _queryAtMessage({ String? messageId, - int limit = 20, + int limit = 40, bool preferOffline = false, }) async { if (channel.state == null) return null; @@ -251,28 +261,32 @@ class StreamChannelState extends State { return null; } - return queryAroundMessage( - messageId, - limit: limit, + return channel.query( + messagesPagination: PaginationParams( + idAround: messageId, + limit: limit, + ), preferOffline: preferOffline, ); } - /// - Future queryAroundMessage( - String messageId, { - @Deprecated('before is deprecated, use limit instead') int before = 20, - @Deprecated('after is deprecated, use limit instead') int after = 20, - int limit = 20, + Future _queryAtTimestamp({ + required DateTime timestamp, + int limit = 40, bool preferOffline = false, - }) => - channel.query( - messagesPagination: PaginationParams( - idAround: messageId, - limit: limit, - ), - preferOffline: preferOffline, - ); + }) async { + if (channel.state == null) return null; + channel.state!.isUpToDate = false; + channel.state!.truncate(); + + return channel.query( + messagesPagination: PaginationParams( + createdAtAround: timestamp.toUtc(), + limit: limit, + ), + preferOffline: preferOffline, + ); + } /// Future queryBeforeMessage( @@ -349,6 +363,15 @@ class StreamChannelState extends State { } } + Future _loadChannelAtTimestamp(DateTime timestamp) async { + try { + await loadChannelAtTimestamp(timestamp); + return true; + } catch (_) { + rethrow; + } + } + @override void initState() { super.initState(); @@ -359,6 +382,18 @@ class StreamChannelState extends State { _futures = [widget.channel.initialized]; if (initialMessageId != null) { _futures.add(_loadChannelAtMessage); + } else if (channel.state != null && channel.state!.unreadCount > 0) { + final read = channel.state!.read.firstWhereOrNull( + (it) => it.user.id == channel.client.state.currentUser?.id, + ); + + if (read != null && + !(channel.state!.messages + .any((it) => it.createdAt.compareTo(read.lastRead) > 0) && + channel.state!.messages + .any((it) => it.createdAt.compareTo(read.lastRead) <= 0))) { + _futures.add(_loadChannelAtTimestamp(read.lastRead)); + } } } @@ -383,7 +418,7 @@ class StreamChannelState extends State { future: Future.wait(_futures), initialData: [ channel.state != null, - if (initialMessageId != null) false, + _futures.length == 1, ], builder: (context, snapshot) { if (snapshot.hasError) { @@ -398,10 +433,9 @@ class StreamChannelState extends State { } return Center(child: Text(message)); } - final initialized = snapshot.data![0]; - // ignore: avoid_bool_literals_in_conditional_expressions - final dataLoaded = initialMessageId == null ? true : snapshot.data![1]; - if (widget.showLoading && (!initialized || !dataLoaded)) { + + final dataLoaded = snapshot.data?.every((it) => it) == true; + if (widget.showLoading && !dataLoaded) { return const Center( child: CircularProgressIndicator(), ); diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart index 210d5e00..7cd425a6 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart @@ -50,9 +50,7 @@ class StreamChannelListController extends PagedValueNotifier { this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, - }) : _activeFilter = filter, - _activeSort = sort, - _eventHandler = eventHandler ?? StreamChannelListEventHandler(), + }) : _eventHandler = eventHandler ?? StreamChannelListEventHandler(), super(const PagedValue.loading()); /// Creates a [StreamChannelListController] from the passed [value]. @@ -66,9 +64,7 @@ class StreamChannelListController extends PagedValueNotifier { this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, - }) : _activeFilter = filter, - _activeSort = sort, - _eventHandler = eventHandler ?? StreamChannelListEventHandler(); + }) : _eventHandler = eventHandler ?? StreamChannelListEventHandler(); /// The client to use for the channels list. final StreamChatClient client; @@ -82,7 +78,6 @@ class StreamChannelListController extends PagedValueNotifier { /// /// You can also filter other built-in channel fields. final Filter? filter; - Filter? _activeFilter; /// The sorting used for the channels matching the filters. /// @@ -94,7 +89,6 @@ class StreamChannelListController extends PagedValueNotifier { /// /// Direction can be ascending or descending. final List>? sort; - List>? _activeSort; /// If true you’ll receive user presence updates via the websocket events final bool presence; @@ -109,18 +103,6 @@ class StreamChannelListController extends PagedValueNotifier { /// Number of members to fetch in each channel. final int? memberLimit; - /// Allows for the change of filters used for channel queries. - /// - /// Use this if you need to support runtime filter changes, - /// through custom filters UI. - set filter(Filter? value) => _activeFilter = value; - - /// Allows for the change of sort used for channel queries. - /// - /// Use this if you need to support runtime sort changes, - /// through custom sort UI. - set sort(List>? value) => _activeSort = value; - @override Future doInitialLoad() async { final limit = min( @@ -129,8 +111,8 @@ class StreamChannelListController extends PagedValueNotifier { ); try { await for (final channels in client.queryChannels( - filter: _activeFilter, - sort: _activeSort, + filter: filter, + sort: sort, memberLimit: memberLimit, messageLimit: messageLimit, presence: presence, @@ -158,8 +140,8 @@ class StreamChannelListController extends PagedValueNotifier { try { await for (final channels in client.queryChannels( - filter: _activeFilter, - sort: _activeSort, + filter: filter, + sort: sort, memberLimit: memberLimit, messageLimit: messageLimit, presence: presence, @@ -181,15 +163,6 @@ class StreamChannelListController extends PagedValueNotifier { } } - @override - Future refresh({bool resetValue = true}) { - if (resetValue) { - _activeFilter = filter; - _activeSort = sort; - } - return super.refresh(resetValue: resetValue); - } - /// Replaces the previously loaded channels with the passed [channels]. set channels(List channels) { if (value.isSuccess) { diff --git a/packages/stream_chat_flutter_core/lib/src/stream_member_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_member_list_controller.dart new file mode 100644 index 00000000..989aa877 --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/stream_member_list_controller.dart @@ -0,0 +1,154 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:stream_chat/stream_chat.dart' hide Success; +import 'package:stream_chat_flutter_core/src/paged_value_notifier.dart'; + +/// The default channel page limit to load. +const defaultMemberPagedLimit = 10; + +const _kDefaultBackendPaginationLimit = 30; + +/// A controller for a member list. +/// +/// This class lets you perform tasks such as: +/// * Load initial data. +/// * Load more data using [loadMore]. +/// * Replace the previously loaded members. +class StreamMemberListController extends PagedValueNotifier { + /// Creates a Stream member list controller. + /// + /// * `client` is the Stream chat client to use for the channels list. + /// + /// * `filter` is the query filters to use. + /// + /// * `sort` is the sorting used for the members matching the filters. + /// + /// * `limit` is the limit to apply to the member list. + StreamMemberListController({ + required this.channel, + this.filter, + this.sort, + this.limit = defaultMemberPagedLimit, + }) : _activeFilter = filter, + _activeSort = sort, + super(const PagedValue.loading()); + + /// Creates a [StreamMemberListController] from the passed [value]. + StreamMemberListController.fromValue( + super.value, { + required this.channel, + this.filter, + this.sort, + this.limit = defaultMemberPagedLimit, + }) : _activeFilter = filter, + _activeSort = sort; + + /// The client to use for the channels list. + final Channel channel; + + /// The query filters to use. + /// + /// You can query on any of the custom fields you've defined on the [Member]. + /// + /// You can also filter other built-in channel fields. + final Filter? filter; + Filter? _activeFilter; + + /// The sorting used for the members matching the filters. + /// + /// Sorting is based on field and direction, multiple sorting options + /// can be provided. + /// + /// Direction can be ascending or descending. + final List? sort; + List? _activeSort; + + /// The limit to apply to the member list. The default is set to + /// [defaultMemberPagedLimit]. + final int limit; + + /// Allows for the change of filters used for member queries. + /// + /// Use this if you need to support runtime filter changes, + /// through custom filters UI. + set filter(Filter? value) => _activeFilter = value; + + /// Allows for the change of the query sort used for member queries. + /// + /// Use this if you need to support runtime sort changes, + /// through custom sort UI. + set sort(List? value) => _activeSort = value; + + @override + Future doInitialLoad() async { + final limit = min( + this.limit * defaultInitialPagedLimitMultiplier, + _kDefaultBackendPaginationLimit, + ); + try { + final memberResponse = await channel.queryMembers( + filter: _activeFilter, + sort: _activeSort, + pagination: PaginationParams(limit: limit), + ); + + final members = memberResponse.members; + final nextKey = members.length < limit ? null : members.length; + value = PagedValue( + items: members.where((it) => it.user != null).toList(), + nextPageKey: nextKey, + ); + } on StreamChatError catch (error) { + value = PagedValue.error(error); + } catch (error) { + final chatError = StreamChatError(error.toString()); + value = PagedValue.error(chatError); + } + } + + @override + Future loadMore(int nextPageKey) async { + final previousValue = value.asSuccess; + + try { + final memberResponse = await channel.queryMembers( + filter: _activeFilter, + sort: _activeSort, + pagination: PaginationParams(limit: limit, offset: nextPageKey), + ); + + final members = memberResponse.members; + final previousItems = previousValue.items; + final newItems = previousItems + members; + final nextKey = members.length < limit ? null : newItems.length; + value = PagedValue( + items: newItems.where((it) => it.user != null).toList(), + nextPageKey: nextKey, + ); + } on StreamChatError catch (error) { + value = previousValue.copyWith(error: error); + } catch (error) { + final chatError = StreamChatError(error.toString()); + value = previousValue.copyWith(error: chatError); + } + } + + @override + Future refresh({bool resetValue = true}) { + if (resetValue) { + _activeFilter = filter; + _activeSort = sort; + } + return super.refresh(resetValue: resetValue); + } + + /// Replaces the previously loaded members with [members] and updates + /// the nextPageKey. + set members(List members) { + value = PagedValue( + items: members, + nextPageKey: members.length, + ); + } +} diff --git a/packages/stream_chat_flutter_core/lib/src/stream_message_input_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_message_input_controller.dart index 9e1586de..ae331e05 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_message_input_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_message_input_controller.dart @@ -11,7 +11,9 @@ import 'package:stream_chat_flutter_core/src/message_text_field_controller.dart' /// Pass in a [StreamMessageInputController] as the `valueListenable`. typedef StreamMessageValueListenableBuilder = ValueListenableBuilder; +/// {@template stream_chat_flutter.StreamMessageInputController} /// Controller for storing and mutating a [Message] value. +/// {@endtemplate} class StreamMessageInputController extends ValueNotifier { /// Creates a controller for an editable text field. /// @@ -50,111 +52,121 @@ class StreamMessageInputController extends ValueNotifier { StreamMessageInputController._({ required Message initialMessage, Map? textPatternStyle, - }) : _textEditingController = MessageTextFieldController.fromValue( - initialMessage.text == null - ? TextEditingValue.empty - : TextEditingValue( - text: initialMessage.text!, - composing: TextRange.collapsed(initialMessage.text!.length), - ), + }) : _initialMessage = initialMessage, + _textFieldController = MessageTextFieldController.fromValue( + _textEditingValueFromMessage(initialMessage), textPatternStyle: textPatternStyle, ), - _initialMessage = initialMessage, super(initialMessage) { - addListener(_textEditingSyncer); + _textFieldController.addListener(_textFieldListener); } - void _textEditingSyncer() { - final cleanText = value.command == null - ? value.text - : value.text?.replaceFirst('/${value.command} ', ''); + /// Returns the controller of the text field linked to this controller. + MessageTextFieldController get textFieldController => _textFieldController; + MessageTextFieldController _textFieldController; - if (cleanText != _textEditingController.text) { - final previousOffset = _textEditingController.value.selection.start; - final previousText = _textEditingController.text; - final diff = (cleanText?.length ?? 0) - previousText.length; - _textEditingController - ..text = cleanText ?? '' - ..selection = TextSelection.collapsed( - offset: previousOffset + diff, - ); + Message _initialMessage; + + static TextEditingValue _textEditingValueFromMessage(Message message) { + final messageText = message.text; + var textEditingValue = TextEditingValue.empty; + if (messageText != null) { + textEditingValue = TextEditingValue( + text: messageText, + selection: TextSelection.collapsed(offset: messageText.length), + ); } + return textEditingValue; + } + + void _textFieldListener() { + final text = _textFieldController.text; + message = message.copyWith(text: text); } /// Returns the current message associated with this controller. Message get message => value; - /// Returns the controller of the text field linked to this controller. - MessageTextFieldController get textEditingController => - _textEditingController; - final MessageTextFieldController _textEditingController; + /// Sets the current message associated with this controller. + set message(Message message) => value = message; - /// Returns the text of the message. - String get text => _textEditingController.text; + @override + set value(Message message) { + super.value = message; - Message _initialMessage; - - /// Sets the message. - set message(Message message) { - value = message; + // Update text field controller only if message text has changed. + final messageText = message.text; + final textFieldText = _textFieldController.text; + if (messageText != textFieldText) { + textEditingValue = _textEditingValueFromMessage(message); + } } - /// Sets the message that's being quoted. - set quotedMessage(Message message) { - value = value.copyWith( - quotedMessage: message, - quotedMessageId: message.id, + /// Text of the message. + String get text => _textFieldController.text; + + /// Sets the text of the message. + set text(String text) { + _textFieldController.text = text; + } + + /// The currently selected [text]. + /// + /// If the selection is collapsed, then this property gives the offset of the + /// cursor within the text. + TextSelection get selection => _textFieldController.selection; + + set selection(TextSelection newSelection) { + _textFieldController.selection = selection; + } + + /// Returns the textEditingValue associated with this controller. + TextEditingValue get textEditingValue => _textFieldController.value; + + set textEditingValue(TextEditingValue value) { + _textFieldController.value = value; + } + + set quotedMessage(Message quotedMessage) { + message = message.copyWith( + quotedMessage: quotedMessage, + quotedMessageId: quotedMessage.id, ); } /// Clears the quoted message. void clearQuotedMessage() { - value = value.copyWith( + message = message.copyWith( quotedMessageId: null, quotedMessage: null, ); } /// Sets a command for the message. - set command(Command command) { - value = value.copyWith( - command: command.name, - text: '/${command.name} ', + set command(String command) { + // Setting the command should also clear the text and attachments. + message = message.copyWith( + text: '', + attachments: [], + command: command, ); } - /// Sets the text of the message. - set text(String newText) { - var newTextWithCommand = newText; - if (value.command != null) { - if (!newText.startsWith('/${value.command}')) { - newTextWithCommand = '/${value.command} $newText'; - } - } - value = value.copyWith(text: newTextWithCommand); - } - - /// Returns the baseOffset of the text field. - int get baseOffset => textEditingController.selection.baseOffset; - - /// Returns the start of the selection of the text field. - int get selectionStart => textEditingController.selection.start; - /// Sets the [showInChannel] flag of the message. set showInChannel(bool newValue) { - value = value.copyWith(showInChannel: newValue); + message = message.copyWith(showInChannel: newValue); } /// Returns true if the message is in a thread and /// should be shown in the main channel as well. - bool get showInChannel => value.showInChannel ?? false; + bool get showInChannel => message.showInChannel ?? false; /// Returns the attachments of the message. - List get attachments => value.attachments; + List get attachments => message.attachments; /// Sets the list of [attachments] for the message. set attachments(List attachments) { - value = value.copyWith(attachments: attachments); + message = message.copyWith(attachments: attachments); } /// Adds a new attachment to the message. @@ -212,11 +224,11 @@ class StreamMessageInputController extends ValueNotifier { } /// Returns the list of mentioned users in the message. - List get mentionedUsers => value.mentionedUsers; + List get mentionedUsers => message.mentionedUsers; /// Sets the mentioned users. set mentionedUsers(List users) { - value = value.copyWith(mentionedUsers: users); + message = message.copyWith(mentionedUsers: users); } /// Adds a user to the list of mentioned users. @@ -239,7 +251,7 @@ class StreamMessageInputController extends ValueNotifier { mentionedUsers = []; } - /// Sets the [message], or [value], to empty. + /// Sets the [message], to empty. /// /// After calling this function, [text], [attachments] and [mentionedUsers] /// will all be empty. @@ -250,23 +262,24 @@ class StreamMessageInputController extends ValueNotifier { /// this method should only be called between frames, e.g. in response to user /// actions, not during the build, layout, or paint phases. void clear() { - value = Message(); - _textEditingController.clear(); + message = Message(); } - /// Sets the [value] to the initial [Message] value. + /// Sets the [message] to the initial [Message] value. void reset({bool resetId = true}) { if (resetId) { final newId = const Uuid().v4(); _initialMessage = _initialMessage.copyWith(id: newId); } - value = _initialMessage; + // Reset the message to the initial value. + message = _initialMessage; } @override void dispose() { - removeListener(_textEditingSyncer); - _textEditingController.dispose(); + _textFieldController + ..removeListener(_textFieldListener) + ..dispose(); super.dispose(); } } @@ -276,7 +289,7 @@ class StreamMessageInputController extends ValueNotifier { /// /// The [StreamMessageInputController] is accessible via the [value] getter. /// During state restoration, -/// the property will restore [StreamMessageInputController.value] +/// the property will restore [StreamMessageInputController.message] /// to the value it had when the restoration data it is getting restored from /// was collected. class StreamRestorableMessageInputController @@ -306,5 +319,5 @@ class StreamRestorableMessageInputController } @override - String toPrimitives() => json.encode(value.value); + String toPrimitives() => json.encode(value.message); } diff --git a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart deleted file mode 100644 index 67841569..00000000 --- a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart +++ /dev/null @@ -1,286 +0,0 @@ -// ignore_for_file: deprecated_member_use_from_same_package - -import 'dart:convert'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; - -/// -/// [UserListCore] is a simplified class that allows fetching users while -/// exposing UI builders. -/// A [UserListController] is used to load and paginate data. -/// -/// ```dart -/// class UsersListPage extends StatelessWidget { -/// @override -/// Widget build(BuildContext context) { -/// return Scaffold( -/// body: UsersListCore( -/// filter: { -/// 'members': { -/// '\$in': [StreamChat.of(context).user.id], -/// } -/// }, -/// sort: [SortOption('last_message_at')], -/// pagination: PaginationParams( -/// limit: 20, -/// ), -/// errorBuilder: (err) { -/// return Center( -/// child: Text('An error has occured'), -/// ); -/// }, -/// emptyBuilder: (context) { -/// return Center( -/// child: Text('Nothing here...'), -/// ); -/// }, -/// emptyBuilder: (context) { -/// return Center( -/// child: CircularProgressIndicator(), -/// ); -/// }, -/// listBuilder: (context, list) { -/// return UsersPage(list); -/// } -/// ), -/// ); -/// } -/// } -/// ``` -/// -/// [UsersBloc] must be the ancestor of this widget. This is necessary since -/// [UserListCore] depends on functionality contained within [UsersBloc]. -/// -/// The parameters [listBuilder], [loadingBuilder], [emptyBuilder] and -/// [errorBuilder] must all be supplied and not null. -@Deprecated(''' -UserListCore is deprecated and will be removed in the next -major version. Use StreamUserListController instead to create your custom list. -More details here https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter_core/stream_user_list_controller -''') -class UserListCore extends StatefulWidget { - /// Instantiate a new [UserListCore] - const UserListCore({ - required this.errorBuilder, - required this.emptyBuilder, - required this.loadingBuilder, - required this.listBuilder, - super.key, - this.filter = const Filter.empty(), - this.sort, - this.presence, - this.groupAlphabetically = false, - this.userListController, - this.limit = 30, - }); - - /// A [UserListController] allows reloading and pagination. - /// Use [UserListController.loadData] and [UserListController.paginateData] - /// respectively for reloading and pagination. - final UserListController? userListController; - - /// The builder that will be used in case of error - final ErrorBuilder errorBuilder; - - /// The builder that will be used to build the list - final Widget Function(BuildContext context, List users) listBuilder; - - /// The builder that will be used for loading - final WidgetBuilder loadingBuilder; - - /// The builder used when the channel list is empty. - final WidgetBuilder emptyBuilder; - - /// The query filters to use. - /// You can query on any of the custom fields you've defined on the [Channel]. - /// You can also filter other built-in channel fields. - final Filter filter; - - /// The sorting used for the channels matching the filters. - /// Sorting is based on field and direction, multiple sorting options can be - /// provided. You can sort based on last_updated, last_message_at, updated_at, - /// created_at or member_count. Direction can be ascending or descending. - final List? sort; - - /// If true you’ll receive user presence updates via the websocket events - final bool? presence; - - /// The amount of users requested per API call. - final int limit; - - /// Set it to true to group users by their first character - /// - /// defaults to false - final bool groupAlphabetically; - - @override - UserListCoreState createState() => UserListCoreState(); -} - -/// The current state of the [UserListCore]. -class UserListCoreState extends State - with WidgetsBindingObserver { - UsersBlocState? _usersBloc; - - @override - void didChangeDependencies() { - final newUsersBloc = UsersBloc.of(context); - if (newUsersBloc != _usersBloc) { - _usersBloc = newUsersBloc; - loadData(); - } - super.didChangeDependencies(); - } - - @override - void initState() { - super.initState(); - _setupController(); - } - - void _setupController() { - if (widget.userListController != null) { - widget.userListController!.loadData = loadData; - widget.userListController!.paginateData = paginateData; - } - } - - @override - Widget build(BuildContext context) => _buildListView(); - - bool get _isListAlreadySorted => - widget.sort?.any((e) => e.field == 'name' && e.direction == 1) ?? false; - - Stream> _buildUserStream() => _usersBloc!.usersStream.map( - (users) { - if (widget.groupAlphabetically) { - var temp = users; - if (!_isListAlreadySorted) { - temp = users - ..sort((curr, next) => curr.name.compareTo(next.name)); - } - final groupedUsers = >{}; - for (final e in temp) { - final alphabet = e.name[0].toUpperCase(); - groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e]; - } - final items = []; - for (final key in groupedUsers.keys) { - items - ..add(ListHeaderItem(key)) - ..addAll(groupedUsers[key]!.map(ListUserItem.new)); - } - return items; - } - return users.map(ListUserItem.new).toList(); - }, - ); - - BetterStreamBuilder> _buildListView() => BetterStreamBuilder( - stream: _buildUserStream(), - errorBuilder: widget.errorBuilder, - noDataBuilder: widget.loadingBuilder, - builder: (context, items) { - if (items.isEmpty) { - return widget.emptyBuilder(context); - } - return widget.listBuilder(context, items); - }, - ); - - /// Fetches initial users and updates the widget - Future loadData() => _usersBloc!.queryUsers( - filter: widget.filter, - sort: widget.sort, - presence: widget.presence, - pagination: PaginationParams(limit: widget.limit), - ); - - /// Fetches more users with updated pagination and updates the widget - Future paginateData() => _usersBloc!.queryUsers( - filter: widget.filter, - sort: widget.sort, - presence: widget.presence, - pagination: PaginationParams( - limit: widget.limit, - offset: _usersBloc!.users?.length ?? 0, - ), - ); - - @override - void didUpdateWidget(UserListCore oldWidget) { - super.didUpdateWidget(oldWidget); - if (jsonEncode(widget.filter) != jsonEncode(oldWidget.filter) || - jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) || - widget.presence != oldWidget.presence || - widget.limit != oldWidget.limit) { - loadData(); - } - - if (widget.userListController != oldWidget.userListController) { - _setupController(); - } - } -} - -/// Represents an item in a the user stream list. -/// Header items are prefixed with the key `HEADER` While users are prefixed -/// with `USER`. -abstract class ListItem { - /// Unique key per list item - String? get key { - if (this is ListHeaderItem) { - final header = (this as ListHeaderItem).heading; - return 'HEADER-${header.toLowerCase()}'; - } - if (this is ListUserItem) { - final user = (this as ListUserItem).user; - return 'USER-${user.id}'; - } - return null; - } - - /// Helper function to build widget based on ListItem type - // ignore: missing_return - Widget when({ - required Widget Function(String heading) headerItem, - required Widget Function(User user) userItem, - }) { - if (this is ListHeaderItem) { - return headerItem((this as ListHeaderItem).heading); - } - if (this is ListUserItem) { - return userItem((this as ListUserItem).user); - } - return Container(); - } -} - -/// Header Item -class ListHeaderItem extends ListItem { - /// Constructs a new [ListHeaderItem] - ListHeaderItem(this.heading); - - /// Heading used to build the item. - final String heading; -} - -/// User Item -class ListUserItem extends ListItem { - /// Constructs a new [ListUserItem] - ListUserItem(this.user); - - /// [User] used to build the item. - final User user; -} - -/// Controller used for paginating data in [ChannelListView] -class UserListController { - /// Call this function to reload data - AsyncCallback? loadData; - - /// Call this function to load further data - AsyncCallback? paginateData; -} diff --git a/packages/stream_chat_flutter_core/lib/src/users_bloc.dart b/packages/stream_chat_flutter_core/lib/src/users_bloc.dart deleted file mode 100644 index 5db4f5f5..00000000 --- a/packages/stream_chat_flutter_core/lib/src/users_bloc.dart +++ /dev/null @@ -1,145 +0,0 @@ -// ignore_for_file: deprecated_member_use_from_same_package - -import 'package:flutter/material.dart'; -import 'package:rxdart/rxdart.dart'; -import 'package:stream_chat_flutter_core/src/stream_controller_extension.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; - -/// Widget dedicated to the management of a users list with pagination. -/// -/// [UsersBloc] can be access at anytime by using the static [of] method -/// using Flutter's [BuildContext]. -/// -/// API docs: https://getstream.io/chat/docs/flutter-dart/init_and_users/ -@Deprecated("Use 'StreamUserListController' instead") -class UsersBloc extends StatefulWidget { - /// Instantiate a new [UsersBloc]. The parameter [child] must be supplied and - /// not null. - const UsersBloc({ - required this.child, - super.key, - }); - - /// The widget child - final Widget child; - - @override - UsersBlocState createState() => UsersBlocState(); - - /// Use this method to get the current [UsersBlocState] instance - static UsersBlocState of(BuildContext context) { - UsersBlocState? state; - - state = context.findAncestorStateOfType(); - - assert( - state != null, - 'You must have a UsersBloc widget as ancestor', - ); - - return state!; - } -} - -/// The current state of the [UsersBloc] -class UsersBlocState extends State - with AutomaticKeepAliveClientMixin { - /// The current users list - List? get users => _usersController.valueOrNull; - - /// The current users list as a stream - Stream> get usersStream => _usersController.stream; - - final _usersController = BehaviorSubject>(); - - final _queryUsersLoadingController = BehaviorSubject.seeded(false); - - /// The stream notifying the state of queryUsers call - Stream get queryUsersLoading => _queryUsersLoadingController.stream; - - late StreamChatCoreState _streamChatCore; - - bool _paginationEnded = false; - - /// The Query Users method allows you to search for users and see if they are - /// online/offline. - /// [API Reference](https://getstream.io/chat/docs/flutter-dart/query_users/?language=dart) - Future queryUsers({ - Filter? filter, - List? sort, - bool? presence, - PaginationParams pagination = const PaginationParams(limit: 30), - }) async { - final client = _streamChatCore.client; - - final offset = pagination.offset; - final clear = offset == null || offset == 0; - - if (clear && _paginationEnded) { - _paginationEnded = false; - } - - if ((!clear && _paginationEnded) || _queryUsersLoadingController.value) { - return; - } - - if (_usersController.hasValue) { - _queryUsersLoadingController.safeAdd(true); - } - - try { - final oldUsers = List.from(users ?? []); - - final usersResponse = await client.queryUsers( - filter: filter, - sort: sort, - presence: presence, - pagination: pagination, - ); - - final newUsers = usersResponse.users; - if (clear) { - _usersController.safeAdd(usersResponse.users); - } else { - final temp = oldUsers + usersResponse.users; - _usersController.safeAdd(temp); - } - if (_usersController.hasValue && _queryUsersLoadingController.value) { - _queryUsersLoadingController.safeAdd(false); - } - if (newUsers.isEmpty || newUsers.length < pagination.limit) { - _paginationEnded = true; - } - } catch (e, stk) { - // reset loading controller - _queryUsersLoadingController.safeAdd(false); - if (_usersController.hasValue) { - _queryUsersLoadingController.safeAddError(e, stk); - } else { - _usersController.safeAddError(e, stk); - } - } - } - - @override - void didChangeDependencies() { - _streamChatCore = StreamChatCore.of(context); - super.didChangeDependencies(); - } - - @override - Widget build(BuildContext context) { - super.build(context); - return widget.child; - } - - @override - void dispose() { - _usersController.close(); - _queryUsersLoadingController.close(); - super.dispose(); - } - - @override - bool get wantKeepAlive => true; -} diff --git a/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart b/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart index 34cbaaa1..e903da77 100644 --- a/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart +++ b/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart @@ -4,12 +4,8 @@ export 'package:connectivity_plus/connectivity_plus.dart'; export 'package:stream_chat/stream_chat.dart'; export 'src/better_stream_builder.dart'; -export 'src/channel_list_core.dart' hide ChannelListCoreState; -export 'src/channels_bloc.dart'; export 'src/lazy_load_scroll_view.dart'; export 'src/message_list_core.dart' hide MessageListCoreState; -export 'src/message_search_bloc.dart'; -export 'src/message_search_list_core.dart' hide MessageSearchListCoreState; export 'src/message_text_field_controller.dart'; export 'src/paged_value_notifier.dart' show PagedValueListenableBuilder, PagedValue, PagedValueNotifier; @@ -18,9 +14,8 @@ export 'src/stream_channel.dart'; export 'src/stream_channel_list_controller.dart'; export 'src/stream_channel_list_event_handler.dart'; export 'src/stream_chat_core.dart'; +export 'src/stream_member_list_controller.dart'; export 'src/stream_message_input_controller.dart'; export 'src/stream_message_search_list_controller.dart'; export 'src/stream_user_list_controller.dart'; export 'src/typedef.dart'; -export 'src/user_list_core.dart' hide UserListCoreState; -export 'src/users_bloc.dart'; diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index a8f1489e..93cb116f 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter_core homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter. -version: 4.6.0 +version: 5.0.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -17,7 +17,7 @@ dependencies: freezed_annotation: ^2.0.3 meta: ^1.3.0 rxdart: ^0.27.0 - stream_chat: ^4.6.0 + stream_chat: ^5.0.0 dev_dependencies: build_runner: ^2.0.1 dart_code_metrics: ^4.4.0 diff --git a/packages/stream_chat_flutter_core/test/channel_list_core_test.dart b/packages/stream_chat_flutter_core/test/channel_list_core_test.dart deleted file mode 100644 index f94ab3c6..00000000 --- a/packages/stream_chat_flutter_core/test/channel_list_core_test.dart +++ /dev/null @@ -1,526 +0,0 @@ -// ignore_for_file: deprecated_member_use_from_same_package - -import 'dart:async'; - -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat_flutter_core/src/channel_list_core.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; - -import 'mocks.dart'; - -void main() { - const pagination = PaginationParams(limit: 3, offset: 0); - - List _generateChannels( - StreamChatClient client, { - int count = 3, - int offset = 0, - }) => - List.generate( - count, - (index) { - index = index + offset; - return Channel( - client, - 'testType$index', - 'testId$index', - extraData: {'extra_data_key': 'extra_data_value_$index'}, - ); - }, - ); - - testWidgets( - 'should throw if ChannelListCore is used where ChannelsBloc is not present ' - 'in the widget tree', - (tester) async { - const channelListCoreKey = Key('channelListCore'); - final channelListCore = ChannelListCore( - key: channelListCoreKey, - listBuilder: (_, __) => const Offstage(), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => const Offstage(), - errorBuilder: (BuildContext context, Object error) => const Offstage(), - ); - - await tester.pumpWidget(channelListCore); - - expect(find.byKey(channelListCoreKey), findsNothing); - expect(tester.takeException(), isInstanceOf()); - }, - ); - - testWidgets( - 'should render ChannelListCore if used with ChannelsBloc as an ancestor', - (tester) async { - const channelListCoreKey = Key('channelListCore'); - final channelListCore = ChannelListCore( - key: channelListCoreKey, - listBuilder: (_, __) => const Offstage(), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => const Offstage(), - errorBuilder: (BuildContext context, Object error) => const Offstage(), - ); - - final mockClient = MockClient(); - - when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => const Stream.empty()); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: ChannelsBloc( - child: channelListCore, - ), - ), - ); - - expect(find.byKey(channelListCoreKey), findsOneWidget); - }, - ); - - testWidgets( - 'should assign loadData and paginateData callback to ' - 'ChannelListController if passed', - (tester) async { - const channelListCoreKey = Key('channelListCore'); - final controller = ChannelListController(); - final channelListCore = ChannelListCore( - key: channelListCoreKey, - listBuilder: (_, __) => const Offstage(), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => const Offstage(), - errorBuilder: (BuildContext context, Object error) => const Offstage(), - channelListController: controller, - ); - - expect(controller.loadData, isNull); - expect(controller.paginateData, isNull); - - final mockClient = MockClient(); - - when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => const Stream.empty()); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: ChannelsBloc( - child: channelListCore, - ), - ), - ); - - expect(find.byKey(channelListCoreKey), findsOneWidget); - expect(controller.loadData, isNotNull); - expect(controller.paginateData, isNotNull); - }, - ); - - testWidgets( - 'should build error widget if channelsBlocState.channelsStream emits error', - (tester) async { - const channelListCoreKey = Key('channelListCore'); - const errorWidgetKey = Key('errorWidget'); - final channelListCore = ChannelListCore( - key: channelListCoreKey, - listBuilder: (_, __) => const Offstage(), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => const Offstage(), - errorBuilder: (BuildContext context, Object error) => - Container(key: errorWidgetKey), - limit: pagination.limit, - ); - - final mockClient = MockClient(); - - when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => const Stream.empty()); - - const error = 'Error! Error! Error!'; - when(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: pagination, - )).thenThrow(error); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: ChannelsBloc( - child: channelListCore, - ), - ), - ); - - await tester.pumpAndSettle(); - - expect(find.byKey(errorWidgetKey), findsOneWidget); - - verify(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: pagination, - )).called(1); - }, - ); - - testWidgets( - '''should build empty widget if channelsBlocState.channelsStream emits empty data''', - (tester) async { - const channelListCoreKey = Key('channelListCore'); - const emptyWidgetKey = Key('emptyWidget'); - final channelListCore = ChannelListCore( - key: channelListCoreKey, - listBuilder: (_, __) => const Offstage(), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => Container(key: emptyWidgetKey), - errorBuilder: (BuildContext context, Object error) => const Offstage(), - limit: pagination.limit, - ); - - final mockClient = MockClient(); - - when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => const Stream.empty()); - - const channels = []; - when(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: pagination, - )).thenAnswer((_) => Stream.value(channels)); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: ChannelsBloc( - child: channelListCore, - ), - ), - ); - - await tester.pumpAndSettle(); - - expect(find.byKey(emptyWidgetKey), findsOneWidget); - - verify(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: pagination, - )).called(1); - }, - ); - - testWidgets( - '''should build list widget if channelsBlocState.channelsStream emits some data''', - (tester) async { - const channelListCoreKey = Key('channelListCore'); - const listWidgetKey = Key('listWidget'); - final channelListCore = ChannelListCore( - key: channelListCoreKey, - listBuilder: (_, __) => Container(key: listWidgetKey), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => const Offstage(), - errorBuilder: (BuildContext context, Object error) => const Offstage(), - limit: pagination.limit, - ); - - final mockClient = MockClient(); - - when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => const Stream.empty()); - - final channels = _generateChannels(mockClient); - when(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: pagination, - )).thenAnswer((_) => Stream.value(channels)); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: ChannelsBloc( - child: channelListCore, - ), - ), - ); - - await tester.pumpAndSettle(); - - expect(find.byKey(listWidgetKey), findsOneWidget); - - verify(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: pagination, - )).called(1); - }, - ); - - testWidgets( - 'should build list widget with paginated data ' - 'on calling channelListCoreState.paginateData', - (tester) async { - const channelListCoreKey = Key('channelListCore'); - const listWidgetKey = Key('listWidget'); - final channelListCore = ChannelListCore( - key: channelListCoreKey, - listBuilder: (_, channels) => Container( - key: listWidgetKey, - child: Text( - channels.map((e) => e.cid).join(','), - ), - ), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => const Offstage(), - errorBuilder: (BuildContext context, Object error) => const Offstage(), - limit: pagination.limit, - ); - - final mockClient = MockClient(); - - when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => const Stream.empty()); - - final channels = _generateChannels(mockClient); - when(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: pagination, - )).thenAnswer((_) => Stream.value(channels)); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: StreamChatCore( - client: mockClient, - child: ChannelsBloc( - child: channelListCore, - ), - ), - ), - ); - - await tester.pumpAndSettle(); - - expect(find.byKey(listWidgetKey), findsOneWidget); - expect(find.text(channels.map((e) => e.cid).join(',')), findsOneWidget); - - verify(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: pagination, - )).called(1); - - final channelListCoreState = tester.state( - find.byKey(channelListCoreKey), - ); - - final offset = channels.length; - final paginatedChannels = _generateChannels(mockClient, offset: offset); - final updatedPagination = pagination.copyWith(offset: offset); - when(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: updatedPagination, - )).thenAnswer((_) => Stream.value(paginatedChannels)); - - await channelListCoreState.paginateData(); - - await tester.pumpAndSettle(); - - expect(find.byKey(listWidgetKey), findsOneWidget); - expect( - find.text([ - ...channels, - ...paginatedChannels, - ].map((e) => e.cid).join(',')), - findsOneWidget, - ); - - verify(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: updatedPagination, - )).called(1); - }, - ); - - testWidgets( - 'should rebuild ChannelListCore with updated widget data ' - 'on calling setState()', - (tester) async { - StateSetter? _stateSetter; - var limit = pagination.limit; - - const channelListCoreKey = Key('channelListCore'); - const listWidgetKey = Key('listWidget'); - - ChannelListCore channelListCoreBuilder(int limit) => ChannelListCore( - key: channelListCoreKey, - listBuilder: (_, channels) => Container( - key: listWidgetKey, - child: Text( - channels.map((e) => e.cid).join(','), - ), - ), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => const Offstage(), - errorBuilder: (BuildContext context, Object error) => - const Offstage(), - limit: limit, - ); - - final mockClient = MockClient(); - - when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => const Stream.empty()); - - final channels = _generateChannels(mockClient); - when(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: pagination, - )).thenAnswer((_) => Stream.value(channels)); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: StreamChatCore( - client: mockClient, - child: ChannelsBloc( - child: StatefulBuilder(builder: (context, stateSetter) { - // Assigning stateSetter for rebuilding ChannelListCore - _stateSetter = stateSetter; - return channelListCoreBuilder(limit); - }), - ), - ), - ), - ); - - await tester.pumpAndSettle(); - - expect(find.byKey(listWidgetKey), findsOneWidget); - expect(find.text(channels.map((e) => e.cid).join(',')), findsOneWidget); - - verify(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: pagination, - )).called(1); - - // Rebuilding ChannelListCore with new pagination limit - _stateSetter?.call(() => limit = 6); - - final updatedChannels = _generateChannels(mockClient, count: limit); - final updatedPagination = PaginationParams(limit: limit, offset: 0); - when(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: updatedPagination, - )).thenAnswer((_) => Stream.value(updatedChannels)); - - await tester.pumpAndSettle(); - - expect(find.byKey(listWidgetKey), findsOneWidget); - expect( - find.text(updatedChannels.map((e) => e.cid).join(',')), - findsOneWidget, - ); - - verify(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: updatedPagination, - )).called(1); - }, - ); - - test('`widget.limit` should match `widget.pagination.limit`', () { - const pagination = PaginationParams(limit: 30); - final channelListCore = ChannelListCore( - listBuilder: (_, __) => const Offstage(), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => const Offstage(), - errorBuilder: (BuildContext context, Object error) => const Offstage(), - limit: pagination.limit, - ); - - expect(channelListCore.limit, pagination.limit); - }); -} diff --git a/packages/stream_chat_flutter_core/test/channels_bloc_test.dart b/packages/stream_chat_flutter_core/test/channels_bloc_test.dart deleted file mode 100644 index 630ef881..00000000 --- a/packages/stream_chat_flutter_core/test/channels_bloc_test.dart +++ /dev/null @@ -1,948 +0,0 @@ -// ignore_for_file: deprecated_member_use_from_same_package - -import 'dart:async'; - -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; - -import 'matchers/channel_matcher.dart'; -import 'mocks.dart'; - -void main() { - setUpAll(() { - registerFallbackValue(const PaginationParams()); - }); - - List _generateChannels( - StreamChatClient client, { - int count = 3, - int offset = 0, - }) => - List.generate( - count, - (index) { - index = index + offset; - return Channel( - client, - 'testType$index', - 'testId$index', - extraData: {'extra_data_key': 'extra_data_value_$index'}, - ); - }, - ); - - testWidgets( - '''should throw if ChannelsBloc is used where StreamChat is not present in the widget tree''', - (tester) async { - const channelsBlocKey = Key('channelsBloc'); - const childKey = Key('child'); - const channelsBloc = ChannelsBloc( - key: channelsBlocKey, - child: Offstage(key: childKey), - ); - - await tester.pumpWidget(channelsBloc); - - expect(find.byKey(channelsBlocKey), findsNothing); - expect(find.byKey(childKey), findsNothing); - expect(tester.takeException(), isInstanceOf()); - }, - ); - - testWidgets( - 'should render ChannelsBloc if used with StreamChatCore as an ancestor', - (tester) async { - const channelsBlocKey = Key('channelsBloc'); - const childKey = Key('child'); - const channelsBloc = ChannelsBloc( - key: channelsBlocKey, - child: Offstage(key: childKey), - ); - - final mockClient = MockClient(); - - when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => const Stream.empty()); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: channelsBloc, - ), - ); - - expect(find.byKey(channelsBlocKey), findsOneWidget); - expect(find.byKey(childKey), findsOneWidget); - }, - ); - - testWidgets( - 'channelsBlocState.queryChannels() should emit data through channelsStream', - (tester) async { - const channelsBlocKey = Key('channelsBloc'); - const childKey = Key('child'); - final channelsBloc = ChannelsBloc( - key: channelsBlocKey, - child: Builder( - key: childKey, - builder: (context) => const Offstage(), - ), - ); - - final mockClient = MockClient(); - - when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => const Stream.empty()); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: channelsBloc, - ), - ); - - final channelsBlocState = tester.state( - find.byKey(channelsBlocKey), - ); - - final offlineChannels = _generateChannels(mockClient); - final onlineChannels = _generateChannels(mockClient, offset: 3); - - when(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: any(named: 'paginationParams'), - )).thenAnswer( - (_) => Stream.fromIterable([offlineChannels, onlineChannels]), - ); - - channelsBlocState.queryChannels(); - - await expectLater( - channelsBlocState.channelsStream, - emitsInOrder([ - isSameChannelListAs(offlineChannels), - isSameChannelListAs(onlineChannels), - ]), - ); - - verify(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: any(named: 'paginationParams'), - )).called(1); - }, - ); - - testWidgets( - 'channelsBlocState.channelsStream should emit error ' - 'if client.queryChannels() throws', - (tester) async { - const channelsBlocKey = Key('channelsBloc'); - const childKey = Key('child'); - final channelsBloc = ChannelsBloc( - key: channelsBlocKey, - child: Builder( - key: childKey, - builder: (context) => const Offstage(), - ), - ); - - final mockClient = MockClient(); - - when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => const Stream.empty()); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: channelsBloc, - ), - ); - - final channelsBlocState = tester.state( - find.byKey(channelsBlocKey), - ); - - const error = 'Error! Error! Error!'; - - when(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: any(named: 'paginationParams'), - )).thenThrow(error); - - channelsBlocState.queryChannels(); - - await expectLater( - channelsBlocState.channelsStream, - emitsError(error), - ); - - verify(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: any(named: 'paginationParams'), - )).called(1); - }, - ); - - testWidgets( - 'calling channelsBlocState.queryChannels() again with an offset ' - 'should emit new data through channelsStream and also emit loading state ' - 'through queryChannelsLoading', - (tester) async { - const channelsBlocKey = Key('channelsBloc'); - const channelsBloc = ChannelsBloc( - key: channelsBlocKey, - child: Offstage(), - ); - - final mockClient = MockClient(); - - when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => const Stream.empty()); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: channelsBloc, - ), - ); - - final channelsBlocState = tester.state( - find.byKey(channelsBlocKey), - ); - - final channels = _generateChannels(mockClient); - - when(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: any(named: 'paginationParams'), - )).thenAnswer((_) => Stream.value(channels)); - - const pagination = PaginationParams(limit: 3); - channelsBlocState.queryChannels( - paginationParams: pagination, - ); - - await expectLater( - channelsBlocState.channelsStream, - emits(isSameChannelListAs(channels)), - ); - - verify(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: any(named: 'paginationParams'), - )).called(1); - - final offset = channels.length; - final paginationParams = pagination.copyWith(offset: offset); - - final newChannels = _generateChannels(mockClient, offset: offset); - - when(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: paginationParams, - )).thenAnswer( - (_) => Stream.value(newChannels), - ); - - channelsBlocState.queryChannels(paginationParams: paginationParams); - - await Future.wait([ - expectLater( - channelsBlocState.queryChannelsLoading, - emitsInOrder([true, false]), - ), - expectLater( - channelsBlocState.channelsStream, - emits(isSameChannelListAs(channels + newChannels)), - ), - ]); - - verify(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: paginationParams, - )).called(1); - }, - ); - - testWidgets( - 'calling channelsBlocState.queryChannels() again with an offset ' - 'should emit error through queryChannelsLoading if ' - 'client.queryChannels() throws', - (tester) async { - const channelsBlocKey = Key('channelsBloc'); - const channelsBloc = ChannelsBloc( - key: channelsBlocKey, - child: Offstage(), - ); - - final mockClient = MockClient(); - - when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => const Stream.empty()); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: channelsBloc, - ), - ); - - final channelsBlocState = tester.state( - find.byKey(channelsBlocKey), - ); - - final channels = _generateChannels(mockClient); - const paginationParams = PaginationParams( - limit: 3, - ); - - when(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: paginationParams, - )).thenAnswer((_) => Stream.value(channels)); - - channelsBlocState.queryChannels( - paginationParams: paginationParams, - ); - - await expectLater( - channelsBlocState.channelsStream, - emits(isSameChannelListAs(channels)), - ); - - verify(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: paginationParams, - )).called(1); - - const error = 'Error! Error! Error!'; - - when(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: paginationParams, - )).thenThrow(error); - - channelsBlocState.queryChannels(paginationParams: paginationParams); - - await expectLater( - channelsBlocState.queryChannelsLoading, - emitsError(error), - ); - - verify(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: paginationParams, - )).called(1); - }, - ); - - group('event controller test', () { - late StreamController eventController; - setUp(() { - eventController = StreamController.broadcast(); - }); - - testWidgets( - 'channel should get hide when EventType.channelHidden event is received', - (tester) async { - final mockClient = MockClient(); - const channelsBlocKey = Key('channelsBloc'); - const channelsBloc = ChannelsBloc( - key: channelsBlocKey, - child: Offstage(), - ); - - when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => const Stream.empty()); - - when(() => mockClient.on( - EventType.channelHidden, - )).thenAnswer((_) => eventController.stream); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: channelsBloc, - ), - ); - - final channelsBlocState = tester.state( - find.byKey(channelsBlocKey), - ); - - final channels = _generateChannels(mockClient); - - when(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: any(named: 'paginationParams'), - )).thenAnswer( - (_) => Stream.value(channels), - ); - - await channelsBlocState.queryChannels(); - - verify(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: any(named: 'paginationParams'), - )).called(1); - - final channelHiddenEvent = Event( - type: EventType.channelHidden, - cid: channels.first.cid, - ); - - eventController.add(channelHiddenEvent); - - final newChannels = [...channels] - ..removeWhere((it) => it.cid == channelHiddenEvent.cid); - - await expectLater( - channelsBlocState.channelsStream, - emitsInOrder([ - isSameChannelListAs(channels), - isSameChannelListAs(newChannels), - ]), - ); - - verify(() => mockClient.on(EventType.channelHidden)).called(1); - }, - ); - - testWidgets( - 'channel should get removed when EventType.channelDeleted or ' - 'EventType.notificationRemovedFromChannel, event is received', - (tester) async { - final mockClient = MockClient(); - const channelsBlocKey = Key('channelsBloc'); - const channelsBloc = ChannelsBloc( - key: channelsBlocKey, - child: Offstage(), - ); - - when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => const Stream.empty()); - - when(() => mockClient.on( - EventType.channelDeleted, - EventType.notificationRemovedFromChannel, - )).thenAnswer((_) => eventController.stream); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: channelsBloc, - ), - ); - - final channelsBlocState = tester.state( - find.byKey(channelsBlocKey), - ); - - final channels = _generateChannels(mockClient); - - when(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: any(named: 'paginationParams'), - )).thenAnswer( - (_) => Stream.value(channels), - ); - - await channelsBlocState.queryChannels(); - - verify(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: any(named: 'paginationParams'), - )).called(1); - - final channelDeletedOrNotificationRemovedEvent = Event( - type: EventType.channelDeleted, - channel: EventChannel( - cid: channels.first.cid!, - updatedAt: DateTime.now(), - config: ChannelConfig(), - createdAt: DateTime.now(), - memberCount: 1, - ), - ); - - eventController.add(channelDeletedOrNotificationRemovedEvent); - - final channelCid = - channelDeletedOrNotificationRemovedEvent.channel?.cid; - final newChannels = [...channels] - ..removeWhere((it) => it.cid == channelCid); - - await expectLater( - channelsBlocState.channelsStream, - emitsInOrder([ - isSameChannelListAs(channels), - isSameChannelListAs(newChannels), - ]), - ); - - verify(() => mockClient.on( - EventType.channelDeleted, - EventType.notificationRemovedFromChannel, - )).called(1); - }, - ); - - testWidgets( - 'event channel should be moved to top of the list if present when ' - 'EventType.messageNew event is received', - (tester) async { - final mockClient = MockClient(); - const channelsBlocKey = Key('channelsBloc'); - const channelsBloc = ChannelsBloc( - key: channelsBlocKey, - child: Offstage(), - ); - - when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => const Stream.empty()); - - when(() => mockClient.on( - EventType.messageNew, - )).thenAnswer((_) => eventController.stream); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: channelsBloc, - ), - ); - - final channelsBlocState = tester.state( - find.byKey(channelsBlocKey), - ); - - final channels = _generateChannels(mockClient); - - when(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: any(named: 'paginationParams'), - )).thenAnswer( - (_) => Stream.value(channels), - ); - - await channelsBlocState.queryChannels(); - - verify(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: any(named: 'paginationParams'), - )).called(1); - - final messageNewEvent = Event( - type: EventType.messageNew, - cid: channels.last.cid, - ); - - eventController.add(messageNewEvent); - - final channelCid = messageNewEvent.cid; - final index = channels.indexWhere((it) => it.cid == channelCid); - final updatedChannel = channels[index]; - final newChannels = [...channels] - ..removeAt(index) - ..insert(0, updatedChannel); - - await expectLater( - channelsBlocState.channelsStream, - emitsInOrder([ - isSameChannelListAs(channels), - isSameChannelListAs(newChannels), - ]), - ); - - verify(() => mockClient.on(EventType.messageNew)).called(1); - }, - ); - - testWidgets( - 'event channel should be moved to top of the list if present inside ' - 'hiddenChannels list and shouldAddChannel is true when ' - 'EventType.messageNew event is received', - (tester) async { - final hiddenChannelEventController = StreamController(); - - addTearDown(hiddenChannelEventController.close); - - final mockClient = MockClient(); - final channels = _generateChannels(mockClient); - const channelsBlocKey = Key('channelsBloc'); - final channelsBloc = ChannelsBloc( - key: channelsBlocKey, - shouldAddChannel: (e) => channels.map((it) => it.cid).contains(e.cid), - child: const Offstage(), - ); - - when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => const Stream.empty()); - - when(() => mockClient.on( - EventType.channelHidden, - )).thenAnswer((_) => hiddenChannelEventController.stream); - - when(() => mockClient.on( - EventType.messageNew, - )).thenAnswer((_) => eventController.stream); - - final messageNewEvent = Event( - type: EventType.messageNew, - cid: channels.last.cid, - ); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: channelsBloc, - ), - ); - - final channelsBlocState = tester.state( - find.byKey(channelsBlocKey), - ); - - when(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: any(named: 'paginationParams'), - )).thenAnswer( - (_) => Stream.value(channels), - ); - - await channelsBlocState.queryChannels(); - - verify(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: any(named: 'paginationParams'), - )).called(1); - - final channelHiddenEvent = Event( - type: EventType.channelHidden, - cid: channels.last.cid, - ); - - // Hiding the channel before passing messageNew event - hiddenChannelEventController.add(channelHiddenEvent); - - final channelsAfterHiddenEvent = [...channels]..removeLast(); - - eventController.add(messageNewEvent); - - final channelCid = messageNewEvent.cid; - final index = channels.indexWhere((it) => it.cid == channelCid); - final newChannels = [...channels] - ..removeAt(index) - ..insert(0, channels[index]); - - await expectLater( - channelsBlocState.channelsStream, - emitsInOrder([ - isSameChannelListAs(channels), - isSameChannelListAs(channelsAfterHiddenEvent), - isSameChannelListAs(newChannels), - ]), - ); - - verify(() => mockClient.on(EventType.channelHidden)).called(1); - verify(() => mockClient.on(EventType.messageNew)).called(1); - }, - ); - - testWidgets( - 'event channel should be moved to top of the list if present inside ' - 'channel state and shouldAddChannel is true when ' - 'EventType.messageNew event is received', - (tester) async { - final mockClient = MockClient(); - final channels = _generateChannels(mockClient); - final stateChannels = { - for (var c in _generateChannels(mockClient, offset: 5)) c.cid!: c - }; - const channelsBlocKey = Key('channelsBloc'); - final channelsBloc = ChannelsBloc( - key: channelsBlocKey, - shouldAddChannel: (_) => true, - child: const Offstage(), - ); - - when(() => mockClient.state.channels).thenReturn(stateChannels); - - when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => const Stream.empty()); - - when(() => mockClient.on( - EventType.messageNew, - )).thenAnswer((_) => eventController.stream); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: channelsBloc, - ), - ); - - final channelsBlocState = tester.state( - find.byKey(channelsBlocKey), - ); - - when(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: any(named: 'paginationParams'), - )).thenAnswer( - (_) => Stream.value(channels), - ); - - await channelsBlocState.queryChannels(); - - verify(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: any(named: 'paginationParams'), - )).called(1); - - final messageNewEvent = Event( - type: EventType.messageNew, - cid: stateChannels.keys.first, - ); - - eventController.add(messageNewEvent); - - final newChannels = [...channels] - ..insert(0, stateChannels[stateChannels.keys.first]!); - - await expectLater( - channelsBlocState.channelsStream, - emitsInOrder([ - isSameChannelListAs(channels), - isSameChannelListAs(newChannels), - ]), - ); - - verify(() => mockClient.on(EventType.messageNew)).called(1); - }, - ); - - testWidgets( - 'channels should get sorted according to channelsComparator when ' - 'EventType.messageNew event is received', - (tester) async { - final mockClient = MockClient(); - final channels = _generateChannels(mockClient); - int channelComparator(Channel a, Channel b) { - final aData = a.extraData['extra_data_key'].toString(); - final bData = b.extraData['extra_data_key'].toString(); - return bData.compareTo(aData); - } - - const channelsBlocKey = Key('channelsBloc'); - final channelsBloc = ChannelsBloc( - key: channelsBlocKey, - shouldAddChannel: (_) => true, - channelsComparator: channelComparator, - child: const Offstage(), - ); - - when(() => mockClient.on(any(), any(), any(), any())) - .thenAnswer((_) => const Stream.empty()); - - when(() => mockClient.on( - EventType.messageNew, - )).thenAnswer((_) => eventController.stream); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: channelsBloc, - ), - ); - - final channelsBlocState = tester.state( - find.byKey(channelsBlocKey), - ); - - when(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: any(named: 'paginationParams'), - )).thenAnswer( - (_) => Stream.value(channels), - ); - - await channelsBlocState.queryChannels(); - - verify(() => mockClient.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: any(named: 'paginationParams'), - )).called(1); - - final messageNewEvent = Event( - type: EventType.messageNew, - cid: channels.first.cid, - ); - - eventController.add(messageNewEvent); - - final newChannels = [...channels]..sort(channelComparator); - - await expectLater( - channelsBlocState.channelsStream, - emitsInOrder([ - isSameChannelListAs(channels), - isSameChannelListAs(newChannels), - ]), - ); - - verify(() => mockClient.on(EventType.messageNew)).called(1); - }, - ); - - tearDown(() { - eventController.close(); - }); - }); -} diff --git a/packages/stream_chat_flutter_core/test/message_list_core_test.dart b/packages/stream_chat_flutter_core/test/message_list_core_test.dart index e61d0abb..d4c92f76 100644 --- a/packages/stream_chat_flutter_core/test/message_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/message_list_core_test.dart @@ -93,7 +93,7 @@ void main() { final mockChannel = MockChannel(); when(() => mockChannel.initialized).thenAnswer((_) => Future.value(true)); - + when(() => mockChannel.state.unreadCount).thenReturn(0); when(() => mockChannel.state.isUpToDate).thenReturn(true); when(() => mockChannel.state.messagesStream) .thenAnswer((_) => Stream.value([])); @@ -129,6 +129,7 @@ void main() { final mockChannel = MockChannel(); when(() => mockChannel.state.isUpToDate).thenReturn(true); + when(() => mockChannel.state.unreadCount).thenReturn(0); when(() => mockChannel.state.messagesStream) .thenAnswer((_) => Stream.value([])); when(() => mockChannel.state.messages).thenReturn([]); @@ -167,6 +168,7 @@ void main() { final mockChannel = MockChannel(); when(() => mockChannel.state.isUpToDate).thenReturn(true); + when(() => mockChannel.state.unreadCount).thenReturn(0); final messages = _generateMessages(); when(() => mockChannel.state.messages).thenReturn(messages); when(() => mockChannel.state.messagesStream) @@ -222,6 +224,7 @@ void main() { when(() => mockChannel.state.messagesStream) .thenAnswer((_) => Stream.error(error)); when(() => mockChannel.state.messages).thenReturn([]); + when(() => mockChannel.state.unreadCount).thenReturn(0); await tester.pumpWidget( Directionality( @@ -263,6 +266,7 @@ void main() { when(() => mockChannel.state.messagesStream) .thenAnswer((_) => Stream.value(messages)); when(() => mockChannel.state.messages).thenReturn(messages); + when(() => mockChannel.state.unreadCount).thenReturn(0); await tester.pumpWidget( Directionality( @@ -312,6 +316,7 @@ void main() { when(() => mockChannel.state.messagesStream) .thenAnswer((_) => Stream.value(messages)); when(() => mockChannel.state.messages).thenReturn(messages); + when(() => mockChannel.state.unreadCount).thenReturn(0); await tester.pumpWidget( Directionality( @@ -357,6 +362,7 @@ void main() { when(() => mockChannel.state.messagesStream) .thenAnswer((_) => Stream.value(messages)); when(() => mockChannel.state.messages).thenReturn(messages); + when(() => mockChannel.state.unreadCount).thenReturn(0); await tester.pumpWidget( Directionality( @@ -406,6 +412,7 @@ void main() { when(() => mockChannel.state.threads).thenReturn(threads); when(() => mockChannel.state.threadsStream) .thenAnswer((_) => Stream.value(threads)); + when(() => mockChannel.state.unreadCount).thenReturn(0); await tester.pumpWidget( Directionality( diff --git a/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart b/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart deleted file mode 100644 index c1465731..00000000 --- a/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart +++ /dev/null @@ -1,400 +0,0 @@ -// ignore_for_file: deprecated_member_use_from_same_package - -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; - -import 'matchers/get_message_response_matcher.dart'; -import 'mocks.dart'; - -const testFilter = Filter.custom(key: 'test', value: 'testValue'); - -void main() { - List _generateMessages({ - int count = 3, - int offset = 0, - }) => - List.generate( - count, - (index) { - index = index + offset; - return GetMessageResponse() - ..message = Message( - id: 'testId$index', - text: 'testTextData$index', - ) - ..channel = ChannelModel( - cid: 'testCid:id', - ); - }, - ); - - testWidgets( - '''messageSearchBlocState.search() should throw if used where StreamChat is not present in the widget tree''', - (tester) async { - const messageSearchBloc = MessageSearchBloc( - child: Offstage(), - ); - - await tester.pumpWidget(messageSearchBloc); - expect(tester.takeException(), isInstanceOf()); - }, - ); - - testWidgets( - 'messageSearchBlocState.search() should emit data through usersStream', - (tester) async { - const messageSearchBlocKey = Key('messageSearchBloc'); - const childKey = Key('child'); - const messageSearchBloc = MessageSearchBloc( - key: messageSearchBlocKey, - child: Offstage(key: childKey), - ); - - final mockClient = MockClient(); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: messageSearchBloc, - ), - ); - final messageSearchBlocState = tester.state( - find.byKey(messageSearchBlocKey), - ); - - final messageResponseList = _generateMessages(); - - when(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), - paginationParams: any(named: 'paginationParams'), - )).thenAnswer( - (_) async => SearchMessagesResponse() - ..results = messageResponseList - ..next = null - ..previous = null, - ); - - messageSearchBlocState.search(filter: testFilter); - - await expectLater( - messageSearchBlocState.messagesStream, - emits(isSameMessageResponseListAs(messageResponseList)), - ); - - verify(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), - paginationParams: any(named: 'paginationParams'), - )).called(1); - }, - ); - - testWidgets( - '''messageSearchBlocState.messagesStream should emit error if client.search() throws''', - (tester) async { - const messageSearchBlocKey = Key('messageSearchBloc'); - const childKey = Key('child'); - const messageSearchBloc = MessageSearchBloc( - key: messageSearchBlocKey, - child: Offstage(key: childKey), - ); - - final mockClient = MockClient(); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: messageSearchBloc, - ), - ); - final messageSearchBlocState = tester.state( - find.byKey(messageSearchBlocKey), - ); - - const error = 'Error! Error! Error!'; - when(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), - paginationParams: any(named: 'paginationParams'), - )).thenThrow(error); - - messageSearchBlocState.search(filter: testFilter); - - await expectLater( - messageSearchBlocState.messagesStream, - emitsError(error), - ); - - verify(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), - paginationParams: any(named: 'paginationParams'), - )).called(1); - }, - ); - - testWidgets( - '''calling messageSearchBlocState.search() again with an offset should emit new data through messagesStream and also emit loading state through queryMessagesLoading''', - (tester) async { - const messageSearchBlocKey = Key('messageSearchBloc'); - const childKey = Key('child'); - const messageSearchBloc = MessageSearchBloc( - key: messageSearchBlocKey, - child: Offstage(key: childKey), - ); - - final mockClient = MockClient(); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: messageSearchBloc, - ), - ); - - final messageSearchBlocState = tester.state( - find.byKey(messageSearchBlocKey), - ); - - const pagination = PaginationParams(limit: 25); - final messageResponseList = _generateMessages(count: 25); - - when(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), - paginationParams: pagination, - )).thenAnswer( - (_) async => SearchMessagesResponse() - ..results = messageResponseList - ..next = null - ..previous = null, - ); - - messageSearchBlocState.search(pagination: pagination, filter: testFilter); - - await expectLater( - messageSearchBlocState.messagesStream, - emits(isSameMessageResponseListAs(messageResponseList)), - ); - - verify(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), - paginationParams: pagination, - )).called(1); - - final offset = messageResponseList.length; - final paginatedMessageResponseList = _generateMessages(offset: offset); - final newPagination = pagination.copyWith(offset: offset); - - when(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), - paginationParams: newPagination, - )).thenAnswer( - (_) async => SearchMessagesResponse() - ..results = paginatedMessageResponseList - ..next = null - ..previous = null, - ); - - messageSearchBlocState.search(pagination: pagination, filter: testFilter); - - await Future.wait([ - expectLater( - messageSearchBlocState.queryMessagesLoading, - emitsInOrder([true, false]), - ), - expectLater( - messageSearchBlocState.messagesStream, - emits(isSameMessageResponseListAs( - messageResponseList + paginatedMessageResponseList, - )), - ), - ]); - - verify(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), - paginationParams: pagination, - )).called(1); - }, - ); - - testWidgets( - '''calling messageSearchBlocState.search() again with an offset should emit error through queryUsersLoading if client.search() throws''', - (tester) async { - const messageSearchBlocKey = Key('messageSearchBloc'); - const childKey = Key('child'); - const messageSearchBloc = MessageSearchBloc( - key: messageSearchBlocKey, - child: Offstage(key: childKey), - ); - - final mockClient = MockClient(); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: messageSearchBloc, - ), - ); - - final messageSearchBlocState = tester.state( - find.byKey(messageSearchBlocKey), - ); - - const pagination = PaginationParams(limit: 25); - final messageResponseList = _generateMessages(count: 25); - - when(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), - paginationParams: pagination, - )).thenAnswer( - (_) async => SearchMessagesResponse() - ..results = messageResponseList - ..next = null - ..previous = null, - ); - - messageSearchBlocState.search(pagination: pagination, filter: testFilter); - - await expectLater( - messageSearchBlocState.messagesStream, - emits(isSameMessageResponseListAs(messageResponseList)), - ); - - verify(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), - paginationParams: pagination, - )).called(1); - - final offset = messageResponseList.length; - final newPagination = pagination.copyWith(offset: offset); - - const error = 'Error! Error! Error!'; - when(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), - paginationParams: newPagination, - )).thenThrow(error); - - messageSearchBlocState.search( - pagination: newPagination, - filter: testFilter, - ); - - await expectLater( - messageSearchBlocState.queryMessagesLoading, - emitsError(error), - ); - - verify(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), - paginationParams: newPagination, - )).called(1); - }, - ); - - testWidgets( - '''calling messageSearchBlocState.search() again with an offset should do nothing and return if pagination is completed''', - (tester) async { - const messageSearchBlocKey = Key('messageSearchBloc'); - const childKey = Key('child'); - const messageSearchBloc = MessageSearchBloc( - key: messageSearchBlocKey, - child: Offstage(key: childKey), - ); - - final mockClient = MockClient(); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: messageSearchBloc, - ), - ); - - final messageSearchBlocState = tester.state( - find.byKey(messageSearchBlocKey), - ); - - const pagination = PaginationParams(limit: 25); - - final messageResponseList = _generateMessages(count: 20); - - when(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), - paginationParams: pagination, - )).thenAnswer( - (_) async => SearchMessagesResponse() - ..results = messageResponseList - ..next = null - ..previous = null, - ); - - messageSearchBlocState.search(pagination: pagination, filter: testFilter); - - await expectLater( - messageSearchBlocState.messagesStream, - emits(isSameMessageResponseListAs(messageResponseList)), - ); - - verify(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), - paginationParams: pagination, - )).called(1); - - final offset = messageResponseList.length; - final newPagination = pagination.copyWith(offset: offset); - - messageSearchBlocState.search( - filter: testFilter, - pagination: newPagination, - ); - - // should emit nothing. - await expectLater( - // skipping the initial data (behaviorSubject). - messageSearchBlocState.messagesStream.skip(1), - emitsInOrder([]), - ); - }, - ); -} diff --git a/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart b/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart deleted file mode 100644 index c56e377b..00000000 --- a/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart +++ /dev/null @@ -1,571 +0,0 @@ -// ignore_for_file: deprecated_member_use_from_same_package - -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat_flutter_core/src/message_search_list_core.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; - -import 'mocks.dart'; - -const testFilter = Filter.custom(key: 'test', value: 'testValue'); -const testMessageFilter = Filter.custom(key: 'test', value: 'testValue'); - -void main() { - List _generateMessages({ - int count = 3, - int offset = 0, - }) => - List.generate( - count, - (index) { - index = index + offset; - return GetMessageResponse() - ..message = Message( - id: 'testId$index', - text: 'testTextData$index', - ) - ..channel = ChannelModel( - cid: 'test:Cid', - ); - }, - ); - - testWidgets( - 'should throw if both `messageQuery` and `messageFilters` are provided', - (tester) async { - expect( - () => MessageSearchListCore( - childBuilder: (_) => const Offstage(), - loadingBuilder: (_) => const Offstage(), - emptyBuilder: (_) => const Offstage(), - errorBuilder: (_, __) => const Offstage(), - filters: testFilter, - messageFilters: testMessageFilter, - messageQuery: 'test', - ), - throwsAssertionError, - ); - }, - ); - - testWidgets( - 'should throw if both `messageQuery` and `messageFilters` are not provided', - (tester) async { - expect( - () => MessageSearchListCore( - childBuilder: (_) => const Offstage(), - loadingBuilder: (_) => const Offstage(), - emptyBuilder: (_) => const Offstage(), - errorBuilder: (_, __) => const Offstage(), - filters: testFilter, - ), - throwsAssertionError, - ); - }, - ); - - testWidgets( - 'should throw if MessageSearchListCore is used where MessageSearchBloc ' - 'is not present in the widget tree', - (tester) async { - const messageSearchListCoreKey = Key('messageSearchListCore'); - final messageSearchListCore = MessageSearchListCore( - key: messageSearchListCoreKey, - childBuilder: (List? messages) => const Offstage(), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => const Offstage(), - errorBuilder: (BuildContext context, Object? error) => const Offstage(), - filters: testFilter, - messageFilters: testMessageFilter, - ); - - await tester.pumpWidget(messageSearchListCore); - - expect(find.byKey(messageSearchListCoreKey), findsNothing); - expect(tester.takeException(), isInstanceOf()); - }, - ); - - testWidgets( - 'should render MessageSearchListCore if used with ' - 'MessageSearchListCore as an ancestor', - (tester) async { - const messageSearchListCoreKey = Key('messageSearchListCore'); - final messageSearchListCore = MessageSearchListCore( - key: messageSearchListCoreKey, - childBuilder: (List messages) => const Offstage(), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => const Offstage(), - errorBuilder: (BuildContext context, Object? error) => const Offstage(), - filters: testFilter, - messageFilters: testMessageFilter, - ); - - final mockClient = MockClient(); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: MessageSearchBloc( - child: messageSearchListCore, - ), - ), - ); - - expect(find.byKey(messageSearchListCoreKey), findsOneWidget); - }, - ); - - testWidgets( - 'should assign loadData and paginateData callback to ' - 'UserListController if passed', - (tester) async { - const messageSearchListCoreKey = Key('messageSearchListCore'); - final controller = MessageSearchListController(); - final messageSearchListCore = MessageSearchListCore( - key: messageSearchListCoreKey, - childBuilder: (List messages) => const Offstage(), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => const Offstage(), - errorBuilder: (BuildContext context, Object error) => const Offstage(), - messageSearchListController: controller, - filters: testFilter, - messageFilters: testMessageFilter, - ); - - expect(controller.loadData, isNull); - expect(controller.paginateData, isNull); - - final mockClient = MockClient(); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: MessageSearchBloc( - child: messageSearchListCore, - ), - ), - ); - await tester.pumpAndSettle(); - - expect(find.byKey(messageSearchListCoreKey), findsOneWidget); - expect(controller.loadData, isNotNull); - expect(controller.paginateData, isNotNull); - }, - ); - - testWidgets( - 'should build error widget if messageSearchBloc.messagesStream emits error', - (tester) async { - const messageSearchListCoreKey = Key('messageSearchListCore'); - const errorWidgetKey = Key('errorWidget'); - final messageSearchListCore = MessageSearchListCore( - key: messageSearchListCoreKey, - childBuilder: (List messages) => const Offstage(), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => const Offstage(), - errorBuilder: (BuildContext context, Object error) => const Offstage( - key: errorWidgetKey, - ), - filters: testFilter, - messageFilters: testMessageFilter, - ); - - final mockClient = MockClient(); - - const error = 'Error! Error! Error!'; - when(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: testMessageFilter, - paginationParams: any(named: 'paginationParams'), - )).thenThrow(error); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: MessageSearchBloc( - child: messageSearchListCore, - ), - ), - ); - - await tester.pumpAndSettle(); - - expect(find.byKey(errorWidgetKey), findsOneWidget); - - verify(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: testMessageFilter, - paginationParams: any(named: 'paginationParams'), - )).called(1); - }, - ); - - testWidgets( - 'should build empty widget if messageSearchBloc.messagesStream ' - 'emits empty data', - (tester) async { - const messageSearchListCoreKey = Key('messageSearchListCore'); - const emptyWidgetKey = Key('emptyWidget'); - final messageSearchListCore = MessageSearchListCore( - key: messageSearchListCoreKey, - childBuilder: (List messages) => const Offstage(), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => - const Offstage(key: emptyWidgetKey), - errorBuilder: (BuildContext context, Object error) => const Offstage(), - filters: testFilter, - messageFilters: testMessageFilter, - ); - - final mockClient = MockClient(); - - final messageResponseList = []; - when(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: testMessageFilter, - paginationParams: any(named: 'paginationParams'), - )).thenAnswer( - (_) async => SearchMessagesResponse() - ..results = messageResponseList - ..next = null - ..previous = null, - ); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: MessageSearchBloc( - child: messageSearchListCore, - ), - ), - ); - - await tester.pumpAndSettle(); - - expect(find.byKey(emptyWidgetKey), findsOneWidget); - - verify(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: testMessageFilter, - paginationParams: any(named: 'paginationParams'), - )).called(1); - }, - ); - - testWidgets( - 'should build child widget if usersBlocState.usersStream emits some data', - (tester) async { - const messageSearchListCoreKey = Key('messageSearchListCore'); - const childWidgetKey = Key('childWidget'); - final messageSearchListCore = MessageSearchListCore( - key: messageSearchListCoreKey, - childBuilder: (List messages) => const Offstage( - key: childWidgetKey, - ), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => const Offstage(), - errorBuilder: (BuildContext context, Object error) => const Offstage(), - filters: testFilter, - messageFilters: testMessageFilter, - ); - - final mockClient = MockClient(); - - final messageResponseList = _generateMessages(); - when(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: testMessageFilter, - paginationParams: any(named: 'paginationParams'), - )).thenAnswer( - (_) async => SearchMessagesResponse() - ..results = messageResponseList - ..next = null - ..previous = null, - ); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: MessageSearchBloc( - child: messageSearchListCore, - ), - ), - ); - - await tester.pumpAndSettle(); - - expect(find.byKey(childWidgetKey), findsOneWidget); - - verify(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: testMessageFilter, - paginationParams: any(named: 'paginationParams'), - )).called(1); - }, - ); - - testWidgets( - 'should build child widget with paginated data ' - 'on calling channelListCoreState.paginateData', - (tester) async { - const messageSearchListCoreKey = Key('messageSearchListCore'); - const childWidgetKey = Key('childWidget'); - const pagination = PaginationParams(limit: 25); - final messageSearchListCore = MessageSearchListCore( - key: messageSearchListCoreKey, - childBuilder: (List messages) => Container( - key: childWidgetKey, - child: Text( - messages.map((e) => '${e.channel?.cid}-${e.message.id}').join(','), - ), - ), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => const Offstage(), - errorBuilder: (BuildContext context, Object error) => const Offstage(), - limit: pagination.limit, - filters: testFilter, - messageFilters: testMessageFilter, - ); - - final mockClient = MockClient(); - - final messageResponseList = _generateMessages(count: 25); - when(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: testMessageFilter, - paginationParams: pagination, - )).thenAnswer( - (_) async => SearchMessagesResponse() - ..results = messageResponseList - ..next = null - ..previous = null, - ); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: StreamChatCore( - client: mockClient, - child: MessageSearchBloc( - child: messageSearchListCore, - ), - ), - ), - ); - - await tester.pumpAndSettle(); - - expect(find.byKey(childWidgetKey), findsOneWidget); - expect( - find.text( - messageResponseList - .map((e) => '${e.channel?.cid}-${e.message.id}') - .join(','), - ), - findsOneWidget, - ); - - verify(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: testMessageFilter, - paginationParams: pagination, - )).called(1); - - final messageSearchListCoreState = - tester.state( - find.byKey(messageSearchListCoreKey), - ); - - final offset = messageResponseList.length; - final paginatedMessageResponseList = _generateMessages(offset: offset); - final updatedPagination = pagination.copyWith(offset: offset); - when(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: testMessageFilter, - paginationParams: updatedPagination, - )).thenAnswer( - (_) async => SearchMessagesResponse() - ..results = paginatedMessageResponseList - ..next = null - ..previous = null, - ); - - await messageSearchListCoreState.paginateData(); - - await tester.pumpAndSettle(); - - expect(find.byKey(childWidgetKey), findsOneWidget); - expect( - find.text([ - ...messageResponseList, - ...paginatedMessageResponseList, - ].map((e) => '${e.channel?.cid}-${e.message.id}').join(',')), - findsOneWidget, - ); - - verify(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: testMessageFilter, - paginationParams: updatedPagination, - )).called(1); - }, - ); - - testWidgets( - 'should rebuild MessageSearchListCore with updated widget data ' - 'on calling setState()', - (tester) async { - const pagination = PaginationParams(); - - StateSetter? _stateSetter; - var limit = pagination.limit; - - const messageSearchListCoreKey = Key('messageSearchListCore'); - const childWidgetKey = Key('childWidget'); - MessageSearchListCore messageSearchListCoreBuilder(int limit) => - MessageSearchListCore( - key: messageSearchListCoreKey, - childBuilder: (List messages) => Container( - key: childWidgetKey, - child: Text( - messages - .map((e) => '${e.channel?.cid}-${e.message.id}') - .join(','), - ), - ), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => const Offstage(), - errorBuilder: (BuildContext context, Object error) => - const Offstage(), - limit: limit, - filters: testFilter, - messageFilters: testMessageFilter, - ); - - final mockClient = MockClient(); - - final messageResponseList = _generateMessages(); - when(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: testMessageFilter, - paginationParams: pagination, - )).thenAnswer( - (_) async => SearchMessagesResponse() - ..results = messageResponseList - ..next = null - ..previous = null, - ); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: StreamChatCore( - client: mockClient, - child: MessageSearchBloc( - child: StatefulBuilder(builder: (context, stateSetter) { - // Assigning stateSetter for rebuilding UserListCore - _stateSetter = stateSetter; - return messageSearchListCoreBuilder(limit); - }), - ), - ), - ), - ); - - await tester.pumpAndSettle(); - - expect(find.byKey(childWidgetKey), findsOneWidget); - expect( - find.text( - messageResponseList - .map((e) => '${e.channel?.cid}-${e.message.id}') - .join(','), - ), - findsOneWidget, - ); - - verify(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: testMessageFilter, - paginationParams: pagination, - )).called(1); - - // Rebuilding MessageSearchListCore with new pagination limit - _stateSetter?.call(() => limit = 6); - - final updatedMessageResponseList = _generateMessages(count: limit); - final updatedPagination = PaginationParams(limit: limit); - when(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: testMessageFilter, - paginationParams: updatedPagination, - )).thenAnswer( - (_) async => SearchMessagesResponse() - ..results = updatedMessageResponseList - ..next = null - ..previous = null, - ); - - await tester.pumpAndSettle(); - - expect(find.byKey(childWidgetKey), findsOneWidget); - expect( - find.text(updatedMessageResponseList - .map((e) => '${e.channel?.cid}-${e.message.id}') - .join(',')), - findsOneWidget, - ); - - verify(() => mockClient.search( - testFilter, - query: any(named: 'query'), - sort: any(named: 'sort'), - messageFilters: testMessageFilter, - paginationParams: updatedPagination, - )).called(1); - }, - ); - - test('`widget.limit` should match `widget.pagination.limit`', () { - const pagination = PaginationParams(limit: 30); - final messageSearchListCore = MessageSearchListCore( - childBuilder: (List messages) => const Offstage(), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => const Offstage(), - errorBuilder: (BuildContext context, Object? error) => const Offstage(), - filters: testFilter, - messageFilters: testMessageFilter, - limit: pagination.limit, - ); - - expect(messageSearchListCore.limit, pagination.limit); - }); -} diff --git a/packages/stream_chat_flutter_core/test/stream_channel_test.dart b/packages/stream_chat_flutter_core/test/stream_channel_test.dart index 673bd4da..86ce7c2b 100644 --- a/packages/stream_chat_flutter_core/test/stream_channel_test.dart +++ b/packages/stream_chat_flutter_core/test/stream_channel_test.dart @@ -64,6 +64,7 @@ void main() { const streamChannelKey = Key('streamChannel'); const childKey = Key('childKey'); when(() => mockChannel.initialized).thenAnswer((_) => Future.value(true)); + when(() => mockChannel.state.unreadCount).thenReturn(0); final streamChannel = StreamChannel( key: streamChannelKey, channel: mockChannel, @@ -97,6 +98,7 @@ void main() { ); when(() => mockChannel.initialized) .thenAnswer((_) => Future.error(error)); + when(() => mockChannel.state.unreadCount).thenReturn(0); await tester.pumpWidget( Directionality( @@ -127,6 +129,7 @@ void main() { ); when(() => mockChannel.initialized).thenAnswer((_) async => false); + when(() => mockChannel.state.unreadCount).thenReturn(0); await tester.pumpWidget( Directionality( diff --git a/packages/stream_chat_flutter_core/test/user_list_core_test.dart b/packages/stream_chat_flutter_core/test/user_list_core_test.dart deleted file mode 100644 index 89d0cdbd..00000000 --- a/packages/stream_chat_flutter_core/test/user_list_core_test.dart +++ /dev/null @@ -1,538 +0,0 @@ -// ignore_for_file: deprecated_member_use_from_same_package - -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat_flutter_core/src/user_list_core.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; - -import 'mocks.dart'; - -void main() { - const alphabets = 'abcdefghijklmnopqrstuvwxyz'; - - List _generateUsers({ - int count = 3, - int offset = 0, - }) => - List.generate( - count, - (index) { - index = index + offset; - return User( - id: 'testId$index', - role: 'testRole$index', - createdAt: DateTime.now(), - updatedAt: DateTime.now(), - lastActive: DateTime.now(), - online: true, - extraData: { - 'name': '${alphabets[index]}-testName', - }, - ); - }, - ); - - testWidgets( - 'should throw if UserListCore is used where UsersBloc is not present ' - 'in the widget tree', - (tester) async { - const userListCoreKey = Key('userListCore'); - final userListCore = UserListCore( - key: userListCoreKey, - listBuilder: (_, __) => const Offstage(), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => const Offstage(), - errorBuilder: (BuildContext context, Object error) => const Offstage(), - ); - - await tester.pumpWidget(userListCore); - - expect(find.byKey(userListCoreKey), findsNothing); - expect(tester.takeException(), isInstanceOf()); - }, - ); - - testWidgets( - 'should render UserListCore if used with UsersBloc as an ancestor', - (tester) async { - const userListCoreKey = Key('userListCore'); - final userListCore = UserListCore( - key: userListCoreKey, - listBuilder: (_, __) => const Offstage(), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => const Offstage(), - errorBuilder: (BuildContext context, Object error) => const Offstage(), - ); - - final mockClient = MockClient(); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: UsersBloc( - child: userListCore, - ), - ), - ); - - expect(find.byKey(userListCoreKey), findsOneWidget); - }, - ); - - testWidgets( - 'should assign loadData and paginateData callback to ' - 'UserListController if passed', - (tester) async { - const userListCoreKey = Key('userListCore'); - final controller = UserListController(); - final userListCore = UserListCore( - key: userListCoreKey, - listBuilder: (_, __) => const Offstage(), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => const Offstage(), - errorBuilder: (BuildContext context, Object error) => const Offstage(), - userListController: controller, - ); - - expect(controller.loadData, isNull); - expect(controller.paginateData, isNull); - - final mockClient = MockClient(); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: UsersBloc( - child: userListCore, - ), - ), - ); - - expect(find.byKey(userListCoreKey), findsOneWidget); - expect(controller.loadData, isNotNull); - expect(controller.paginateData, isNotNull); - }, - ); - - testWidgets( - 'should build error widget if usersBlocState.usersStream emits error', - (tester) async { - const userListCoreKey = Key('userListCore'); - const errorWidgetKey = Key('errorWidget'); - final userListCore = UserListCore( - key: userListCoreKey, - listBuilder: (_, __) => const Offstage(), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => const Offstage(), - errorBuilder: (BuildContext context, Object error) => - Container(key: errorWidgetKey), - ); - - final mockClient = MockClient(); - - const error = 'Error! Error! Error!'; - when(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: any(named: 'pagination'), - )).thenThrow(error); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: UsersBloc( - child: userListCore, - ), - ), - ); - - await tester.pumpAndSettle(); - - expect(find.byKey(errorWidgetKey), findsOneWidget); - - verify(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: any(named: 'pagination'), - )).called(1); - }, - ); - - testWidgets( - 'should build empty widget if usersBlocState.usersStream emits empty data', - (tester) async { - const userListCoreKey = Key('userListCore'); - const emptyWidgetKey = Key('emptyWidget'); - final userListCore = UserListCore( - key: userListCoreKey, - listBuilder: (_, __) => const Offstage(), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => Container(key: emptyWidgetKey), - errorBuilder: (BuildContext context, Object error) => const Offstage(), - ); - - final mockClient = MockClient(); - - const users = []; - when(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: any(named: 'pagination'), - )).thenAnswer((_) async => QueryUsersResponse()..users = users); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: UsersBloc( - child: userListCore, - ), - ), - ); - - await tester.pumpAndSettle(); - - expect(find.byKey(emptyWidgetKey), findsOneWidget); - - verify(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: any(named: 'pagination'), - )).called(1); - }, - ); - - testWidgets( - 'should build list widget if usersBlocState.usersStream emits some data', - (tester) async { - const userListCoreKey = Key('userListCore'); - const listWidgetKey = Key('listWidget'); - final userListCore = UserListCore( - key: userListCoreKey, - listBuilder: (_, __) => Container(key: listWidgetKey), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => const Offstage(), - errorBuilder: (BuildContext context, Object error) => const Offstage(), - ); - - final mockClient = MockClient(); - - final users = _generateUsers(); - when(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: any(named: 'pagination'), - )).thenAnswer((_) async => QueryUsersResponse()..users = users); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: UsersBloc( - child: userListCore, - ), - ), - ); - - await tester.pumpAndSettle(); - - expect(find.byKey(listWidgetKey), findsOneWidget); - - verify(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: any(named: 'pagination'), - )).called(1); - }, - ); - - testWidgets( - 'should build list widget with grouped data if groupAlphabetically is true', - (tester) async { - const userListCoreKey = Key('userListCore'); - const listWidgetKey = Key('listWidget'); - final userListCore = UserListCore( - key: userListCoreKey, - listBuilder: (_, items) => Container( - key: listWidgetKey, - child: ListView( - children: items - .map((e) => Container( - key: Key(e.key ?? ''), - child: e.when( - headerItem: Text.new, - userItem: (user) => Text(user.id), - ), - )) - .toList(growable: false), - ), - ), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => const Offstage(), - errorBuilder: (BuildContext context, Object error) => const Offstage(), - groupAlphabetically: true, - ); - - final mockClient = MockClient(); - - final users = _generateUsers(); - when(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: any(named: 'pagination'), - )).thenAnswer((_) async => QueryUsersResponse()..users = users); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: StreamChatCore( - client: mockClient, - child: UsersBloc( - child: userListCore, - ), - ), - ), - ); - - await tester.pumpAndSettle(); - - expect(find.byKey(listWidgetKey), findsOneWidget); - for (final user in users) { - expect(find.byKey(Key('HEADER-${user.name[0]}')), findsOneWidget); - expect(find.byKey(Key('USER-${user.id}')), findsOneWidget); - } - - verify(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: any(named: 'pagination'), - )).called(1); - }, - ); - - testWidgets( - 'should build list widget with paginated data ' - 'on calling channelListCoreState.paginateData', - (tester) async { - const userListCoreKey = Key('userListCore'); - const listWidgetKey = Key('listWidget'); - const pagination = PaginationParams(limit: 15); - final userListCore = UserListCore( - key: userListCoreKey, - listBuilder: (_, items) => Container( - key: listWidgetKey, - child: ListView( - children: items - .map((e) => Container( - key: Key(e.key ?? ''), - child: e.when( - headerItem: Text.new, - userItem: (user) => Text(user.id), - ), - )) - .toList(growable: false), - ), - ), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => const Offstage(), - errorBuilder: (BuildContext context, Object error) => const Offstage(), - limit: pagination.limit, - groupAlphabetically: true, - ); - - final mockClient = MockClient(); - - final users = _generateUsers(count: 15); - when(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: any(named: 'pagination'), - )).thenAnswer((_) async => QueryUsersResponse()..users = users); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: StreamChatCore( - client: mockClient, - child: UsersBloc( - child: userListCore, - ), - ), - ), - ); - - await tester.pumpAndSettle(); - - expect(find.byKey(listWidgetKey), findsOneWidget); - for (final user in users) { - expect(find.byKey(Key('HEADER-${user.name[0]}')), findsOneWidget); - expect(find.byKey(Key('USER-${user.id}')), findsOneWidget); - } - - verify(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: any(named: 'pagination'), - )).called(1); - - final userListCoreState = tester.state( - find.byKey(userListCoreKey), - ); - - final offset = users.length; - final paginatedUsers = _generateUsers(offset: offset); - final updatedPagination = pagination.copyWith(offset: offset); - when(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: updatedPagination, - )) - .thenAnswer( - (_) async => QueryUsersResponse()..users = paginatedUsers); - - await userListCoreState.paginateData(); - - await tester.pumpAndSettle(); - - for (final user in users + paginatedUsers) { - expect(find.byKey(Key('HEADER-${user.name[0]}')), findsOneWidget); - expect(find.byKey(Key('USER-${user.id}')), findsOneWidget); - } - - verify(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: updatedPagination, - )).called(1); - }, - ); - - testWidgets( - 'should rebuild UserListCore with updated widget data ' - 'on calling setState()', - (tester) async { - const pagination = PaginationParams(); - - StateSetter? _stateSetter; - var limit = pagination.limit; - - const userListCoreKey = Key('userListCore'); - const listWidgetKey = Key('listWidget'); - UserListCore userListCoreBuilder(int limit) => UserListCore( - key: userListCoreKey, - listBuilder: (_, items) => Container( - key: listWidgetKey, - child: ListView( - children: items - .map((e) => Container( - key: Key(e.key ?? ''), - child: e.when( - headerItem: Text.new, - userItem: (user) => Text(user.id), - ), - )) - .toList(growable: false), - ), - ), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => const Offstage(), - errorBuilder: (BuildContext context, Object error) => - const Offstage(), - limit: limit, - groupAlphabetically: true, - ); - - final mockClient = MockClient(); - - final users = _generateUsers(); - when(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: any(named: 'pagination'), - )).thenAnswer((_) async => QueryUsersResponse()..users = users); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: StreamChatCore( - client: mockClient, - child: UsersBloc( - child: StatefulBuilder(builder: (context, stateSetter) { - // Assigning stateSetter for rebuilding UserListCore - _stateSetter = stateSetter; - return userListCoreBuilder(limit); - }), - ), - ), - ), - ); - - await tester.pumpAndSettle(); - - expect(find.byKey(listWidgetKey), findsOneWidget); - for (final user in users) { - expect(find.byKey(Key('HEADER-${user.name[0]}')), findsOneWidget); - expect(find.byKey(Key('USER-${user.id}')), findsOneWidget); - } - - verify(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: any(named: 'pagination'), - )).called(1); - - // Rebuilding UserListCore with new pagination limit - _stateSetter?.call(() => limit = 6); - - final updatedUsers = _generateUsers(count: limit); - final updatedPagination = PaginationParams(limit: limit); - when(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: updatedPagination, - )) - .thenAnswer((_) async => QueryUsersResponse()..users = updatedUsers); - - await tester.pumpAndSettle(); - - for (final user in updatedUsers) { - expect(find.byKey(Key('HEADER-${user.name[0]}')), findsOneWidget); - expect(find.byKey(Key('USER-${user.id}')), findsOneWidget); - } - - verify(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: updatedPagination, - )).called(1); - }, - ); - - test('`widget.limit` should match `widget.pagination.limit`', () { - const limit = 20; - final userListCore = UserListCore( - listBuilder: (_, __) => const Offstage(), - loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => const Offstage(), - errorBuilder: (BuildContext context, Object error) => const Offstage(), - limit: limit, - ); - - expect(userListCore.limit, limit); - }); -} diff --git a/packages/stream_chat_flutter_core/test/users_bloc_test.dart b/packages/stream_chat_flutter_core/test/users_bloc_test.dart deleted file mode 100644 index ce28c2f8..00000000 --- a/packages/stream_chat_flutter_core/test/users_bloc_test.dart +++ /dev/null @@ -1,365 +0,0 @@ -// ignore_for_file: deprecated_member_use_from_same_package - -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_flutter_core/src/stream_chat_core.dart'; -import 'package:stream_chat_flutter_core/src/users_bloc.dart'; - -import 'matchers/users_matcher.dart'; -import 'mocks.dart'; - -void main() { - List _generateUsers({ - int count = 3, - int offset = 0, - }) => - List.generate( - count, - (index) { - index = index + offset; - return User( - id: 'testId$index', - role: 'testRole$index', - createdAt: DateTime.now(), - updatedAt: DateTime.now(), - lastActive: DateTime.now(), - online: true, - extraData: const {'extra_data_key': 'extraDataValue'}, - ); - }, - ); - - testWidgets( - 'usersBlocState.queryUsers() should throw if used where ' - 'StreamChat is not present in the widget tree', - (tester) async { - const usersBloc = UsersBloc( - child: Offstage(), - ); - - await tester.pumpWidget(usersBloc); - expect(tester.takeException(), isInstanceOf()); - }, - ); - - testWidgets( - 'usersBlocState.queryUsers() should emit data through usersStream', - (tester) async { - const usersBlocKey = Key('usersBloc'); - const childKey = Key('child'); - const usersBloc = UsersBloc( - key: usersBlocKey, - child: Offstage(key: childKey), - ); - - final mockClient = MockClient(); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: usersBloc, - ), - ); - - final usersBlocState = tester.state( - find.byKey(usersBlocKey), - ); - - final users = _generateUsers(); - - when(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: any(named: 'pagination'), - )).thenAnswer((_) async => QueryUsersResponse()..users = users); - - usersBlocState.queryUsers(); - - await expectLater( - usersBlocState.usersStream, - emits(isSameUserListAs(users)), - ); - - verify(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: any(named: 'pagination'), - )).called(1); - }, - ); - - testWidgets( - 'usersBlocState.usersStream should emit error ' - 'if client.queryUsers() throws', - (tester) async { - const usersBlocKey = Key('usersBloc'); - const childKey = Key('child'); - const usersBloc = UsersBloc( - key: usersBlocKey, - child: Offstage(key: childKey), - ); - - final mockClient = MockClient(); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: usersBloc, - ), - ); - - final usersBlocState = tester.state( - find.byKey(usersBlocKey), - ); - - const error = 'Error! Error! Error!'; - - when(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: any(named: 'pagination'), - )).thenThrow(error); - - usersBlocState.queryUsers(); - - await expectLater( - usersBlocState.usersStream, - emitsError(error), - ); - - verify(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: any(named: 'pagination'), - )).called(1); - }, - ); - - testWidgets( - 'calling usersBlocState.queryUsers() again with an offset ' - 'should emit new data through usersStream and also emit loading state ' - 'through queryUsersLoading', - (tester) async { - const usersBlocKey = Key('usersBloc'); - const childKey = Key('child'); - const usersBloc = UsersBloc( - key: usersBlocKey, - child: Offstage(key: childKey), - ); - - final mockClient = MockClient(); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: usersBloc, - ), - ); - - final usersBlocState = tester.state( - find.byKey(usersBlocKey), - ); - - const pagination = PaginationParams(limit: 25); - final users = _generateUsers(count: 25); - - when(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: pagination, - )).thenAnswer((_) async => QueryUsersResponse()..users = users); - - usersBlocState.queryUsers(pagination: pagination); - - await expectLater( - usersBlocState.usersStream, - emits(isSameUserListAs(users)), - ); - - verify(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: pagination, - )).called(1); - - final offset = users.length; - final paginatedUsers = _generateUsers(offset: offset); - final newPagination = pagination.copyWith(offset: offset); - - when(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: newPagination, - )).thenAnswer( - (_) async => QueryUsersResponse()..users = paginatedUsers, - ); - - usersBlocState.queryUsers(pagination: newPagination); - - await Future.wait([ - expectLater( - usersBlocState.queryUsersLoading, - emitsInOrder([true, false]), - ), - expectLater( - usersBlocState.usersStream, - emits(isSameUserListAs(users + paginatedUsers)), - ), - ]); - - verify(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: newPagination, - )).called(1); - }, - ); - - testWidgets( - 'calling usersBlocState.queryUsers() again with an offset ' - 'should emit error through queryUsersLoading if ' - 'client.queryUsers() throws', - (tester) async { - const usersBlocKey = Key('usersBloc'); - const childKey = Key('child'); - const usersBloc = UsersBloc( - key: usersBlocKey, - child: Offstage(key: childKey), - ); - - final mockClient = MockClient(); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: usersBloc, - ), - ); - - final usersBlocState = tester.state( - find.byKey(usersBlocKey), - ); - - const pagination = PaginationParams(limit: 25); - final users = _generateUsers(count: 25); - - when(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: pagination, - )).thenAnswer((_) async => QueryUsersResponse()..users = users); - - usersBlocState.queryUsers(pagination: pagination); - - await expectLater( - usersBlocState.usersStream, - emits(isSameUserListAs(users)), - ); - - verify(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: pagination, - )).called(1); - - final offset = users.length; - final newPagination = pagination.copyWith(offset: offset); - - const error = 'Error! Error! Error!'; - - when(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: newPagination, - )).thenThrow(error); - - usersBlocState.queryUsers(pagination: newPagination); - - await expectLater( - usersBlocState.queryUsersLoading, - emitsError(error), - ); - - verify(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: newPagination, - )).called(1); - }, - ); - - testWidgets( - '''calling usersBlocState.queryUsers() again with an offset should do nothing and return if pagination is completed''', - (tester) async { - const usersBlocKey = Key('usersBloc'); - const childKey = Key('child'); - const usersBloc = UsersBloc( - key: usersBlocKey, - child: Offstage(key: childKey), - ); - - final mockClient = MockClient(); - - await tester.pumpWidget( - StreamChatCore( - client: mockClient, - child: usersBloc, - ), - ); - - final usersBlocState = tester.state( - find.byKey(usersBlocKey), - ); - - const pagination = PaginationParams(limit: 30); - final users = _generateUsers(count: 25); - - when(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: pagination, - )).thenAnswer((_) async => QueryUsersResponse()..users = users); - - usersBlocState.queryUsers(); - - await expectLater( - usersBlocState.usersStream, - emits(isSameUserListAs(users)), - ); - - verify(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: any(named: 'pagination'), - )).called(1); - - final offset = users.length; - final newPagination = pagination.copyWith(offset: offset); - - usersBlocState.queryUsers(pagination: newPagination); - - // should emit nothing. - await expectLater( - // skipping the initial data (behaviorSubject). - usersBlocState.usersStream, - emitsInOrder([]), - ); - }, - ); -} diff --git a/packages/stream_chat_localizations/CHANGELOG.md b/packages/stream_chat_localizations/CHANGELOG.md index 850fba35..20c9aaef 100644 --- a/packages/stream_chat_localizations/CHANGELOG.md +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -1,3 +1,25 @@ +## 4.0.0 + +🔄 Changed + +* Removed `emojiMatchingQueryText` string. + +## 4.0.0-beta.2 + +* Included the changes from version [3.3.0](#330). + +## 4.0.0-beta.1 + +✅ Added + +* `couldNotReadBytesFromFileError` with translations +* `downloadLabel` with translations +* `toggleMuteUnmuteAction` with translations +* `toggleMuteUnmuteGroupQuestion` with translations +* `toggleMuteUnmuteGroupText` with translations +* `toggleMuteUnmuteUserQuestion` with translations +* `toggleMuteUnmuteUserText` with translations + ## 3.3.0 * Added support for [Norwegian](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart) locale. diff --git a/packages/stream_chat_localizations/example/lib/add_new_lang.dart b/packages/stream_chat_localizations/example/lib/add_new_lang.dart index 6ca047f1..c52761a8 100644 --- a/packages/stream_chat_localizations/example/lib/add_new_lang.dart +++ b/packages/stream_chat_localizations/example/lib/add_new_lang.dart @@ -152,7 +152,8 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { 'The file is too large to upload. The file size limit is $limitInMB MB.'; @override - String emojiMatchingQueryText(String query) => 'Emoji matching "$query"'; + String get couldNotReadBytesFromFileError => + 'Could not read bytes from file.'; @override String get addAFileLabel => 'Add a file'; @@ -396,6 +397,54 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { @override String get slowModeOnLabel => 'Slow mode ON'; + @override + String get downloadLabel => 'Download'; + + @override + String toggleMuteUnmuteUserText({required bool isMuted}) { + if (isMuted) { + return 'Unmute User'; + } else { + return 'Mute User'; + } + } + + @override + String toggleMuteUnmuteGroupQuestion({required bool isMuted}) { + if (isMuted) { + return 'Are you sure you want to unmute this group?'; + } else { + return 'Are you sure you want to mute this group?'; + } + } + + @override + String toggleMuteUnmuteUserQuestion({required bool isMuted}) { + if (isMuted) { + return 'Are you sure you want to unmute this user?'; + } else { + return 'Are you sure you want to mute this user?'; + } + } + + @override + String toggleMuteUnmuteAction({required bool isMuted}) { + if (isMuted) { + return 'UNMUTE'; + } else { + return 'MUTE'; + } + } + + @override + String toggleMuteUnmuteGroupText({required bool isMuted}) { + if (isMuted) { + return 'Unmute Group'; + } else { + return 'Mute Group'; + } + } + @override String get linkDisabledDetails => 'Sending links is not allowed in this conversation.'; @@ -413,6 +462,12 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { } return '$unreadCount unread messages'; } + + @override + String get enableFileAccessMessage => 'Enable file access to continue'; + + @override + String get allowFileAccessMessage => 'Allow access to files'; } void main() async { @@ -476,36 +531,38 @@ class MyApp extends StatelessWidget { final Channel channel; @override - Widget build(BuildContext context) => MaterialApp( - theme: ThemeData.light(), - darkTheme: ThemeData.dark(), - // Add all the supported locales - supportedLocales: const [ - Locale('en'), - Locale('hi'), - Locale('fr'), - Locale('it'), - Locale('es'), - Locale('ja'), - Locale('ko'), - // Add support for additional 'nn' locale - Locale('nn'), - ], - // Add overridden "NnStreamChatLocalizations.delegate" along with - // "GlobalStreamChatLocalizations.delegates" - localizationsDelegates: const [ - NnStreamChatLocalizations.delegate, - ...GlobalStreamChatLocalizations.delegates, - ], - builder: (context, widget) => StreamChat( - client: client, - child: widget, - ), - home: StreamChannel( - channel: channel, - child: const ChannelPage(), - ), - ); + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData.light(), + darkTheme: ThemeData.dark(), + // Add all the supported locales + supportedLocales: const [ + Locale('en'), + Locale('hi'), + Locale('fr'), + Locale('it'), + Locale('es'), + Locale('ja'), + Locale('ko'), + // Add support for additional 'nn' locale + Locale('nn'), + ], + // Add overridden "NnStreamChatLocalizations.delegate" along with + // "GlobalStreamChatLocalizations.delegates" + localizationsDelegates: const [ + NnStreamChatLocalizations.delegate, + ...GlobalStreamChatLocalizations.delegates, + ], + builder: (context, widget) => StreamChat( + client: client, + child: widget, + ), + home: StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ); + } } /// A list of messages sent in the current channel. @@ -521,15 +578,17 @@ class ChannelPage extends StatelessWidget { }); @override - Widget build(BuildContext context) => Scaffold( - appBar: const StreamChannelHeader(), - body: Column( - children: const [ - Expanded( - child: StreamMessageListView(), - ), - StreamMessageInput(), - ], - ), - ); + Widget build(BuildContext context) { + return Scaffold( + appBar: const StreamChannelHeader(), + body: Column( + children: const [ + Expanded( + child: StreamMessageListView(), + ), + StreamMessageInput(), + ], + ), + ); + } } diff --git a/packages/stream_chat_localizations/example/lib/main.dart b/packages/stream_chat_localizations/example/lib/main.dart index a212c82c..ae8aa2b9 100644 --- a/packages/stream_chat_localizations/example/lib/main.dart +++ b/packages/stream_chat_localizations/example/lib/main.dart @@ -105,15 +105,17 @@ class ChannelPage extends StatelessWidget { }); @override - Widget build(BuildContext context) => Scaffold( - appBar: const StreamChannelHeader(), - body: Column( - children: const [ - Expanded( - child: StreamMessageListView(), - ), - StreamMessageInput(), - ], - ), - ); + Widget build(BuildContext context) { + return Scaffold( + appBar: const StreamChannelHeader(), + body: Column( + children: const [ + Expanded( + child: StreamMessageListView(), + ), + StreamMessageInput(), + ], + ), + ); + } } diff --git a/packages/stream_chat_localizations/example/lib/override_lang.dart b/packages/stream_chat_localizations/example/lib/override_lang.dart index f3621915..c37094cc 100644 --- a/packages/stream_chat_localizations/example/lib/override_lang.dart +++ b/packages/stream_chat_localizations/example/lib/override_lang.dart @@ -132,15 +132,17 @@ class ChannelPage extends StatelessWidget { }); @override - Widget build(BuildContext context) => Scaffold( - appBar: const StreamChannelHeader(), - body: Column( - children: const [ - Expanded( - child: StreamMessageListView(), - ), - StreamMessageInput(), - ], - ), - ); + Widget build(BuildContext context) { + return Scaffold( + appBar: const StreamChannelHeader(), + body: Column( + children: const [ + Expanded( + child: StreamMessageListView(), + ), + StreamMessageInput(), + ], + ), + ); + } } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart index cda64eed..a1464ad8 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart @@ -3,24 +3,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -part 'stream_chat_localizations_es.dart'; - -part 'stream_chat_localizations_en.dart'; - -part 'stream_chat_localizations_fr.dart'; - -part 'stream_chat_localizations_it.dart'; - -part 'stream_chat_localizations_ja.dart'; - -part 'stream_chat_localizations_ko.dart'; - -part 'stream_chat_localizations_hi.dart'; - -part 'stream_chat_localizations_pt.dart'; - part 'stream_chat_localizations_de.dart'; - +part 'stream_chat_localizations_en.dart'; +part 'stream_chat_localizations_es.dart'; +part 'stream_chat_localizations_fr.dart'; +part 'stream_chat_localizations_hi.dart'; +part 'stream_chat_localizations_it.dart'; +part 'stream_chat_localizations_ja.dart'; +part 'stream_chat_localizations_ko.dart'; +part 'stream_chat_localizations_pt.dart'; part 'stream_chat_localizations_no.dart'; /// The set of supported languages, as language code strings. diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart index 0ad26f34..0c0d5660 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart @@ -124,9 +124,6 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { 'Die Datei ist zu groß zum Hochladen. ' 'Die Dateigröße ist begrenzt auf $limitInMB MB.'; - @override - String emojiMatchingQueryText(String query) => 'Emoji-Abgleich "$query"'; - @override String get addAFileLabel => 'Datei hinzufügen'; @@ -379,6 +376,54 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { String get sendMessagePermissionError => 'Sie sind nicht berechtigt Nachrichten zu senden'; + @override + String get couldNotReadBytesFromFileError => + 'Kan bytes niet uit bestand lezen.'; + + @override + String get downloadLabel => 'Downloaden'; + + @override + String toggleMuteUnmuteAction({required bool isMuted}) { + if (isMuted) { + return 'UNMUTE'; + } else { + return 'STOM'; + } + } + + @override + String toggleMuteUnmuteGroupQuestion({required bool isMuted}) { + if (isMuted) { + return 'Weet je zeker dat je het dempen van deze groep wilt opheffen?'; + } else { + return 'Weet je zeker dat je deze groep wilt dempen?'; + } + } + + @override + String toggleMuteUnmuteGroupText({required bool isMuted}) { + if (isMuted) { + return 'Dempen groep opheffen'; + } else { + return 'Groep dempen'; + } + } + + @override + String toggleMuteUnmuteUserQuestion({required bool isMuted}) { + if (isMuted) { + return '''Weet je zeker dat je het dempen van deze gebruiker wilt opheffen?'''; + } else { + return 'Weet u zeker dat u deze gebruiker wilt dempen?'; + } + } + + @override + String toggleMuteUnmuteUserText({required bool isMuted}) { + return 'Gebruiker dempen'; + } + @override String get viewLibrary => 'Bibliothek öffnen'; @@ -389,4 +434,12 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { } return '$unreadCount ungelesene Nachrichten'; } + + @override + String get enableFileAccessMessage => + 'Bitte aktivieren Sie den Zugriff auf Dateien,' + '\ndamit Sie sie mit Freunden teilen können.'; + + @override + String get allowFileAccessMessage => 'Zugriff auf Dateien zulassen'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart index cf00e816..34cf5fef 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart @@ -128,7 +128,8 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { 'The file is too large to upload. The file size limit is $limitInMB MB.'; @override - String emojiMatchingQueryText(String query) => 'Emoji matching "$query"'; + String get couldNotReadBytesFromFileError => + 'Could not read bytes from file.'; @override String get addAFileLabel => 'Add a file'; @@ -372,6 +373,54 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { @override String get slowModeOnLabel => 'Slow mode ON'; + @override + String get downloadLabel => 'Download'; + + @override + String toggleMuteUnmuteUserText({required bool isMuted}) { + if (isMuted) { + return 'Unmute User'; + } else { + return 'Mute User'; + } + } + + @override + String toggleMuteUnmuteGroupQuestion({required bool isMuted}) { + if (isMuted) { + return 'Are you sure you want to unmute this group?'; + } else { + return 'Are you sure you want to mute this group?'; + } + } + + @override + String toggleMuteUnmuteUserQuestion({required bool isMuted}) { + if (isMuted) { + return 'Are you sure you want to unmute this user?'; + } else { + return 'Are you sure you want to mute this user?'; + } + } + + @override + String toggleMuteUnmuteAction({required bool isMuted}) { + if (isMuted) { + return 'UNMUTE'; + } else { + return 'MUTE'; + } + } + + @override + String toggleMuteUnmuteGroupText({required bool isMuted}) { + if (isMuted) { + return 'Unmute Group'; + } else { + return 'Mute Group'; + } + } + @override String get linkDisabledDetails => 'Sending links is not allowed in this conversation.'; @@ -389,4 +438,11 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { } return '$unreadCount unread messages'; } + + @override + String get enableFileAccessMessage => 'Please enable access to files' + '\nso you can share them with friends.'; + + @override + String get allowFileAccessMessage => 'Allow access to files'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart index 3e85123d..8bbf5775 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart @@ -132,8 +132,8 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { 'El límite de tamaño de los archivos es de $limitInMB MB.'; @override - String emojiMatchingQueryText(String query) => - 'Emoji que corresponde a "$query"'; + String get couldNotReadBytesFromFileError => + 'No se pudieron leer los bytes del archivo.'; @override String get addAFileLabel => 'Añadir un archivo'; @@ -381,6 +381,54 @@ No es posible añadir más de $limit archivos adjuntos @override String get slowModeOnLabel => 'Modo lento activado'; + @override + String get downloadLabel => 'Descargar'; + + @override + String toggleMuteUnmuteUserText({required bool isMuted}) { + if (isMuted) { + return 'No silenciar usuario'; + } else { + return 'Usuario mudo'; + } + } + + @override + String toggleMuteUnmuteGroupQuestion({required bool isMuted}) { + if (isMuted) { + return '¿Estás seguro de que quieres activar el silencio de este grupo?'; + } else { + return '¿Estás seguro de que quieres silenciar a este grupo?'; + } + } + + @override + String toggleMuteUnmuteUserQuestion({required bool isMuted}) { + if (isMuted) { + return '¿Estás seguro de que quieres activar el sonido de este usuario?'; + } else { + return '¿Estás seguro de que quieres silenciar a este usuario?'; + } + } + + @override + String toggleMuteUnmuteAction({required bool isMuted}) { + if (isMuted) { + return 'DESACTIVAR'; + } else { + return 'SILENCIO'; + } + } + + @override + String toggleMuteUnmuteGroupText({required bool isMuted}) { + if (isMuted) { + return 'Activar grupo'; + } else { + return 'Silenciar grupo'; + } + } + @override String get linkDisabledDetails => 'No se permite enviar enlaces en esta conversación.'; @@ -395,4 +443,11 @@ No es posible añadir más de $limit archivos adjuntos } return '$unreadCount mensajes no leídos'; } + + @override + String get enableFileAccessMessage => 'Habilite el acceso a los archivos' + '\npara poder compartirlos con amigos.'; + + @override + String get allowFileAccessMessage => 'Permitir el acceso a los archivos'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart index c41547c3..16834958 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart @@ -131,8 +131,8 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { 'La taille limite du fichier est de $limitInMB Mo.'; @override - String emojiMatchingQueryText(String query) => - 'Emoji qui correspond à "$query"'; + String get couldNotReadBytesFromFileError => + 'Impossible de lire les octets du fichier.'; @override String get addAFileLabel => 'Ajouter un fichier'; @@ -380,6 +380,54 @@ Limite de pièces jointes dépassée : il n'est pas possible d'ajouter plus de $ @override String get slowModeOnLabel => 'Mode lent activé'; + @override + String get downloadLabel => 'Télécharger'; + + @override + String toggleMuteUnmuteUserText({required bool isMuted}) { + if (isMuted) { + return "Réactiver l'utilisateur"; + } else { + return 'Utilisateur muet'; + } + } + + @override + String toggleMuteUnmuteGroupQuestion({required bool isMuted}) { + if (isMuted) { + return 'Voulez-vous vraiment réactiver le son de ce groupe ?'; + } else { + return '¿Estás seguro de que quieres silenciar a este grupo?'; + } + } + + @override + String toggleMuteUnmuteUserQuestion({required bool isMuted}) { + if (isMuted) { + return 'Voulez-vous vraiment réactiver le son de cet utilisateur ?'; + } else { + return 'Voulez-vous vraiment désactiver cet utilisateur ?'; + } + } + + @override + String toggleMuteUnmuteAction({required bool isMuted}) { + if (isMuted) { + return 'RÉACTIVER LE MUET'; + } else { + return 'MUET'; + } + } + + @override + String toggleMuteUnmuteGroupText({required bool isMuted}) { + if (isMuted) { + return 'Activer le groupe'; + } else { + return 'Groupe muet'; + } + } + @override String get linkDisabledDetails => "L'envoi de liens n'est pas autorisé dans cette conversation."; @@ -394,4 +442,12 @@ Limite de pièces jointes dépassée : il n'est pas possible d'ajouter plus de $ } return '$unreadCount messages non lus'; } + + @override + String get enableFileAccessMessage => + "Veuillez autoriser l'accès aux fichiers" + '\nafin de pouvoir les partager avec des amis.'; + + @override + String get allowFileAccessMessage => "Autoriser l'accès aux fichiers"; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart index be1153e9..0887f57f 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart @@ -127,7 +127,7 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { 'फ़ाइल अपलोड करने के लिए बहुत बड़ी है। फ़ाइल आकार सीमा $limitInMB MB है।'; @override - String emojiMatchingQueryText(String query) => '"$query" से मिलते हुए इमोजी'; + String get couldNotReadBytesFromFileError => 'फ़ाइल से बाइट नहीं पढ़ सका.'; @override String get addAFileLabel => 'एक फ़ाइल जोड़ें'; @@ -374,6 +374,54 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { @override String get slowModeOnLabel => 'स्लो मोड चालू'; + @override + String get downloadLabel => 'डाउनलोड'; + + @override + String toggleMuteUnmuteUserText({required bool isMuted}) { + if (isMuted) { + return 'उपयोगकर्ता को अनम्यूट करें'; + } else { + return 'उपयोगकर्ता को म्यूट करें'; + } + } + + @override + String toggleMuteUnmuteGroupQuestion({required bool isMuted}) { + if (isMuted) { + return 'क्या आप वाकई इस समूह को अनम्यूट करना चाहते हैं?'; + } else { + return 'क्या आप वाकई इस समूह को म्यूट करना चाहते हैं?'; + } + } + + @override + String toggleMuteUnmuteUserQuestion({required bool isMuted}) { + if (isMuted) { + return 'क्या आप वाकई इस उपयोगकर्ता को अनम्यूट करना चाहते हैं?'; + } else { + return 'क्या आप वाकई इस उपयोगकर्ता को म्यूट करना चाहते हैं?'; + } + } + + @override + String toggleMuteUnmuteAction({required bool isMuted}) { + if (isMuted) { + return 'अनम्यूट'; + } else { + return 'मूक'; + } + } + + @override + String toggleMuteUnmuteGroupText({required bool isMuted}) { + if (isMuted) { + return 'समूह अनम्यूट करें'; + } else { + return 'मूक समूह'; + } + } + @override String get linkDisabledDetails => 'इस बातचीत में लिंक भेजने की अनुमति नहीं है.'; @@ -388,4 +436,11 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { } return '$unreadCount अपठित संदेश'; } + + @override + String get enableFileAccessMessage => 'कृपया फ़ाइलों तक पहुंच सक्षम करें ताकि' + '\nआप उन्हें मित्रों के साथ साझा कर सकें।'; + + @override + String get allowFileAccessMessage => 'फाइलों तक पहुंच की अनुमति दें'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart index eb62d655..3e5d7d62 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart @@ -135,7 +135,8 @@ class StreamChatLocalizationsIt extends GlobalStreamChatLocalizations { Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.'''; @override - String emojiMatchingQueryText(String query) => 'Emoji per "$query"'; + String get couldNotReadBytesFromFileError => + 'Impossibile leggere i byte dal file.'; @override String get addAFileLabel => 'Aggiungi un file'; @@ -382,6 +383,54 @@ Attenzione: il limite massimo di $limit file è stato superato. @override String get slowModeOnLabel => 'Slowmode attiva'; + @override + String get downloadLabel => 'Scaricamento'; + + @override + String toggleMuteUnmuteUserText({required bool isMuted}) { + if (isMuted) { + return "Attiva l'audio dell'utente"; + } else { + return 'Utente muto'; + } + } + + @override + String toggleMuteUnmuteGroupQuestion({required bool isMuted}) { + if (isMuted) { + return 'Sei sicuro di voler riattivare questo gruppo?'; + } else { + return 'Sei sicuro di voler disattivare questo gruppo?'; + } + } + + @override + String toggleMuteUnmuteUserQuestion({required bool isMuted}) { + if (isMuted) { + return 'Sei sicuro di voler riattivare questo utente?'; + } else { + return 'Sei sicuro di voler silenziare questo utente?'; + } + } + + @override + String toggleMuteUnmuteAction({required bool isMuted}) { + if (isMuted) { + return 'RIATTIVATO'; + } else { + return 'MUTO'; + } + } + + @override + String toggleMuteUnmuteGroupText({required bool isMuted}) { + if (isMuted) { + return 'Riattiva gruppo'; + } else { + return 'Gruppo muto'; + } + } + @override String get linkDisabledDetails => 'Non è permesso condividere link in questa convesazione.'; @@ -396,4 +445,11 @@ Attenzione: il limite massimo di $limit file è stato superato. } return '$unreadCount messaggi non letti'; } + + @override + String get enableFileAccessMessage => "Per favore attiva l'accesso ai file" + '\ncosí potrai condividerli con i tuoi amici.'; + + @override + String get allowFileAccessMessage => "Consenti l'accesso ai file"; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart index 3e5d5b37..b6384d4b 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart @@ -124,7 +124,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { 'ファイルが大きすぎてアップロードできません。ファイルサイズの制限は${limitInMB}MBです。'; @override - String emojiMatchingQueryText(String query) => '「"$query"」とお揃いの絵文字'; + String get couldNotReadBytesFromFileError => 'ファイルからバイトを読み取れませんでした'; @override String get addAFileLabel => 'ファイルの追加'; @@ -360,6 +360,54 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { 添付ファイルの制限を超えました:$limit個のファイル以上を添付することはできません '''; + @override + String get downloadLabel => 'ダウンロード'; + + @override + String toggleMuteUnmuteUserText({required bool isMuted}) { + if (isMuted) { + return 'ユーザーのミュートを解除する'; + } else { + return 'ユーザーをミュート'; + } + } + + @override + String toggleMuteUnmuteGroupQuestion({required bool isMuted}) { + if (isMuted) { + return 'このグループのミュートを解除してもよろしいですか?'; + } else { + return 'このグループをミュートしてもよろしいですか?'; + } + } + + @override + String toggleMuteUnmuteUserQuestion({required bool isMuted}) { + if (isMuted) { + return 'このユーザーのミュートを解除してもよろしいですか?'; + } else { + return 'このユーザーをミュートしてもよろしいですか?'; + } + } + + @override + String toggleMuteUnmuteAction({required bool isMuted}) { + if (isMuted) { + return 'ミュートを解除する'; + } else { + return 'ミュート'; + } + } + + @override + String toggleMuteUnmuteGroupText({required bool isMuted}) { + if (isMuted) { + return 'グループのミュートを解除'; + } else { + return 'ミュートグループ'; + } + } + @override String get linkDisabledDetails => 'この会話では、リンクの送信は許可されていません。'; @@ -373,4 +421,11 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { } return '$unreadCountつの未読メッセージ'; } + + @override + String get enableFileAccessMessage => + '友達と共有できるように、' '\nファイルへのアクセスを有効にしてください。'; + + @override + String get allowFileAccessMessage => 'ファイルへのアクセスを許可する'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart index 519cd3cf..b1b929a4 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart @@ -124,7 +124,7 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { '파일이 너무 커서 업로드할 수 없습니다. 파일 크기 제한은 ${limitInMB}MB입니다.'; @override - String emojiMatchingQueryText(String query) => '"$query"과 일치하는 이모티콘입니다'; + String get couldNotReadBytesFromFileError => '파일에서 바이트를 읽을 수 없습니다.'; @override String get addAFileLabel => '파일을 추가함'; @@ -361,6 +361,54 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { String attachmentLimitExceedError(int limit) => '첨부 파일 제한 초과: $limit 이상의 첨부 파일을 추가할 수 없습니다'; + @override + String get downloadLabel => '다운로드'; + + @override + String toggleMuteUnmuteUserText({required bool isMuted}) { + if (isMuted) { + return '사용자 음소거 해제'; + } else { + return '사용자 음소거'; + } + } + + @override + String toggleMuteUnmuteGroupQuestion({required bool isMuted}) { + if (isMuted) { + return '이 그룹의 음소거를 해제하시겠습니까?'; + } else { + return '이 그룹을 음소거하시겠습니까?'; + } + } + + @override + String toggleMuteUnmuteUserQuestion({required bool isMuted}) { + if (isMuted) { + return '이 사용자의 음소거를 해제하시겠습니까?'; + } else { + return '이 사용자를 음소거하시겠습니까?'; + } + } + + @override + String toggleMuteUnmuteAction({required bool isMuted}) { + if (isMuted) { + return '음소거 해제'; + } else { + return '무음'; + } + } + + @override + String toggleMuteUnmuteGroupText({required bool isMuted}) { + if (isMuted) { + return '그룹 음소거 해제'; + } else { + return '음소거 그룹'; + } + } + @override String get linkDisabledDetails => '이 대화에서는 링크를 보낼 수 없습니다.'; @@ -374,4 +422,10 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { } return '읽지 않은 메시지 $unreadCount개'; } + + @override + String get enableFileAccessMessage => '친구와 공유할 수 있도록 파일에 대한 액세스를 허용하세요.'; + + @override + String get allowFileAccessMessage => '파일에 대한 액세스 허용'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart index f0abb76a..db231a86 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart @@ -127,9 +127,6 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { String fileTooLargeError(double limitInMB) => 'Filen er for stor til å laste opp. Filgrense er $limitInMB MB.'; - @override - String emojiMatchingQueryText(String query) => 'Emoji matcher "$query"'; - @override String get addAFileLabel => 'Legg til en fil'; @@ -389,4 +386,53 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { } return '$unreadCount uleste meldinger'; } + + @override + String get couldNotReadBytesFromFileError => + 'Kunne ikke lese bytes fra filen.'; + + @override + String get downloadLabel => 'Nedlasting'; + + @override + String toggleMuteUnmuteAction({required bool isMuted}) { + if (isMuted) return 'Slå på lyden for bruker'; + return 'Dempe bruker'; + } + + @override + String toggleMuteUnmuteGroupQuestion({required bool isMuted}) { + if (isMuted) { + return 'Er du sikker på at du vil oppheve ignoreringen av denne gruppen?'; + } + return 'Er du sikker på at du vil ignorere denne gruppen?'; + } + + @override + String toggleMuteUnmuteGroupText({required bool isMuted}) { + if (isMuted) return 'Slå på lyden for gruppe'; + return 'Mute gruppe'; + } + + @override + String toggleMuteUnmuteUserQuestion({required bool isMuted}) { + if (isMuted) { + // ignore: lines_longer_than_80_chars + return 'Er du sikker på at du vil oppheve ignoreringen av denne brukeren?'; + } + return 'Er du sikker på at du vil ignorere denne brukeren?'; + } + + @override + String toggleMuteUnmuteUserText({required bool isMuted}) { + if (isMuted) return 'Opphev lyden av brukeren'; + return 'Dempe brukeren'; + } + + @override + String get enableFileAccessMessage => + 'Aktiver tilgang til filer slik' '\nat du kan dele dem med venner.'; + + @override + String get allowFileAccessMessage => 'Gi tilgang til filer'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart index d761a45b..254a3d5e 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart @@ -126,8 +126,8 @@ class StreamChatLocalizationsPt extends GlobalStreamChatLocalizations { 'O tamanho máximo dos arquivos é de $limitInMB MB.'; @override - String emojiMatchingQueryText(String query) => - 'Emoji correspondente a "$query"'; + String get couldNotReadBytesFromFileError => + 'Não foi possível ler os bytes do arquivo.'; @override String get addAFileLabel => 'Adicionar um arquivo'; @@ -371,6 +371,54 @@ Não é possível adicionar mais de $limit arquivos de uma vez @override String get slowModeOnLabel => 'Modo lento ativado'; + @override + String get downloadLabel => 'Download'; + + @override + String toggleMuteUnmuteUserText({required bool isMuted}) { + if (isMuted) { + return 'Ativar o som do usuário'; + } else { + return 'Silenciar usuário'; + } + } + + @override + String toggleMuteUnmuteGroupQuestion({required bool isMuted}) { + if (isMuted) { + return 'Tem certeza de que deseja ativar o som deste grupo?'; + } else { + return 'Tem certeza de que deseja silenciar este grupo?'; + } + } + + @override + String toggleMuteUnmuteUserQuestion({required bool isMuted}) { + if (isMuted) { + return 'Tem certeza de que deseja ativar o som deste usuário?'; + } else { + return 'Tem certeza de que deseja silenciar este usuário?'; + } + } + + @override + String toggleMuteUnmuteAction({required bool isMuted}) { + if (isMuted) { + return 'ATIVAR MUDO'; + } else { + return 'MUDO'; + } + } + + @override + String toggleMuteUnmuteGroupText({required bool isMuted}) { + if (isMuted) { + return 'Reativar o som do grupo'; + } else { + return 'Silenciar Grupo'; + } + } + @override String get linkDisabledDetails => 'O envio de links não é permitido nesta conversa.'; @@ -392,4 +440,11 @@ Não é possível adicionar mais de $limit arquivos de uma vez } return '$unreadCount mensagens não lidas'; } + + @override + String get enableFileAccessMessage => + 'Ative o acesso aos arquivos' '\npara poder compartilhá-los com amigos.'; + + @override + String get allowFileAccessMessage => 'Permitir acesso aos arquivos'; } diff --git a/packages/stream_chat_localizations/pubspec.yaml b/packages/stream_chat_localizations/pubspec.yaml index 88f4ab6a..10056a1a 100644 --- a/packages/stream_chat_localizations/pubspec.yaml +++ b/packages/stream_chat_localizations/pubspec.yaml @@ -1,6 +1,6 @@ name: stream_chat_localizations description: The Official localizations for Stream Chat Flutter, a service for building chat applications -version: 3.3.0 +version: 4.0.0 homepage: https://github.com/GetStream/stream-chat-flutter repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -14,8 +14,8 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - stream_chat_flutter: ^4.6.0 + stream_chat_flutter: ^5.0.0 dev_dependencies: - dart_code_metrics: ^4.4.0 + dart_code_metrics: ^4.16.0 flutter_test: sdk: flutter diff --git a/packages/stream_chat_localizations/test/translations_test.dart b/packages/stream_chat_localizations/test/translations_test.dart index 669fd285..4004994a 100644 --- a/packages/stream_chat_localizations/test/translations_test.dart +++ b/packages/stream_chat_localizations/test/translations_test.dart @@ -63,7 +63,6 @@ void main() { expect(localizations.instantCommandsLabel, isNotNull); expect(localizations.fileTooLargeAfterCompressionError(33), isNotNull); expect(localizations.fileTooLargeError(33), isNotNull); - expect(localizations.emojiMatchingQueryText('sahil'), isNotNull); expect(localizations.addAFileLabel, isNotNull); expect(localizations.photoFromCameraLabel, isNotNull); expect(localizations.uploadAFileLabel, isNotNull); @@ -178,6 +177,27 @@ void main() { expect(localizations.fileText, isNotNull); expect(localizations.replyToMessageLabel, isNotNull); expect(localizations.attachmentLimitExceedError(3), isNotNull); + expect( + localizations.galleryPaginationText(currentPage: 1, totalPages: 2), + isNotNull, + ); + expect(localizations.slowModeOnLabel, isNotNull); + expect(localizations.linkDisabledDetails, isNotNull); + expect(localizations.linkDisabledError, isNotNull); + expect(localizations.sendMessagePermissionError, isNotNull); + expect(localizations.couldNotReadBytesFromFileError, isNotNull); + expect(localizations.toggleMuteUnmuteAction(isMuted: false), isNotNull); + expect(localizations.downloadLabel, isNotNull); + expect(localizations.toggleMuteUnmuteGroupQuestion(isMuted: true), + isNotNull); + expect(localizations.toggleMuteUnmuteGroupText(isMuted: true), isNotNull); + expect( + localizations.toggleMuteUnmuteUserQuestion(isMuted: true), isNotNull); + expect(localizations.toggleMuteUnmuteUserText(isMuted: true), isNotNull); + expect(localizations.viewLibrary, isNotNull); + expect(localizations.unreadMessagesSeparatorText(2), isNotNull); + expect(localizations.enableFileAccessMessage, isNotNull); + expect(localizations.allowFileAccessMessage, isNotNull); }); } diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index 3d1f0184..f85d13b7 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,3 +1,11 @@ +## 5.0.0 + +- Included the changes from version [4.3.0](#430) and [4.4.0](#440). + +## 5.0.0-beta.1 + +- Updated `stream_chat` dependency to [`5.0.0-beta.1`](https://pub.dev/packages/stream_chat/changelog). + ## 4.4.0 - Allowed experimental use of indexedDb on web with `webUseExperimentalIndexedDb` parameter on `StreamChatPersistenceClient`. @@ -128,4 +136,4 @@ ## 1.0.0-beta -* Initial release +* Initial release \ No newline at end of file diff --git a/packages/stream_chat_persistence/example/android/.gitignore b/packages/stream_chat_persistence/example/android/.gitignore index 0a741cb4..6f568019 100644 --- a/packages/stream_chat_persistence/example/android/.gitignore +++ b/packages/stream_chat_persistence/example/android/.gitignore @@ -9,3 +9,5 @@ GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app key.properties +**/*.keystore +**/*.jks diff --git a/packages/stream_chat_persistence/example/android/app/build.gradle b/packages/stream_chat_persistence/example/android/app/build.gradle index 31d3553b..5fe3c929 100644 --- a/packages/stream_chat_persistence/example/android/app/build.gradle +++ b/packages/stream_chat_persistence/example/android/app/build.gradle @@ -26,21 +26,26 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 31 + compileSdkVersion flutter.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } sourceSets { main.java.srcDirs += 'src/main/kotlin' } - lintOptions { - disable 'InvalidPackage' - } - defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.example" - minSdkVersion 16 - targetSdkVersion 29 + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/packages/stream_chat_persistence/example/android/app/src/main/AndroidManifest.xml b/packages/stream_chat_persistence/example/android/app/src/main/AndroidManifest.xml index 9b3997fe..3f41384d 100644 --- a/packages/stream_chat_persistence/example/android/app/src/main/AndroidManifest.xml +++ b/packages/stream_chat_persistence/example/android/app/src/main/AndroidManifest.xml @@ -1,16 +1,12 @@ - - - - diff --git a/packages/stream_chat_persistence/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/stream_chat_persistence/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/packages/stream_chat_persistence/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/stream_chat_persistence/example/android/app/src/main/res/values-night/styles.xml b/packages/stream_chat_persistence/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..3db14bb5 --- /dev/null +++ b/packages/stream_chat_persistence/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/stream_chat_persistence/example/android/app/src/main/res/values/styles.xml b/packages/stream_chat_persistence/example/android/app/src/main/res/values/styles.xml index 1f83a33f..d460d1e9 100644 --- a/packages/stream_chat_persistence/example/android/app/src/main/res/values/styles.xml +++ b/packages/stream_chat_persistence/example/android/app/src/main/res/values/styles.xml @@ -1,7 +1,7 @@ - - diff --git a/packages/stream_chat_persistence/example/android/build.gradle b/packages/stream_chat_persistence/example/android/build.gradle index b4ef1adb..4256f917 100644 --- a/packages/stream_chat_persistence/example/android/build.gradle +++ b/packages/stream_chat_persistence/example/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.6.0' + ext.kotlin_version = '1.6.10' repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:4.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -14,7 +14,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/packages/stream_chat_persistence/example/android/gradle.properties b/packages/stream_chat_persistence/example/android/gradle.properties index a6738207..94adc3a3 100644 --- a/packages/stream_chat_persistence/example/android/gradle.properties +++ b/packages/stream_chat_persistence/example/android/gradle.properties @@ -1,4 +1,3 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true -android.enableR8=true diff --git a/packages/stream_chat_persistence/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/stream_chat_persistence/example/android/gradle/wrapper/gradle-wrapper.properties index de2ccd60..bc6a58af 100644 --- a/packages/stream_chat_persistence/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/stream_chat_persistence/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/packages/stream_chat_persistence/example/lib/main.dart b/packages/stream_chat_persistence/example/lib/main.dart index 5771d716..6e71a555 100644 --- a/packages/stream_chat_persistence/example/lib/main.dart +++ b/packages/stream_chat_persistence/example/lib/main.dart @@ -65,10 +65,12 @@ class StreamExample extends StatelessWidget { final Channel channel; @override - Widget build(BuildContext context) => MaterialApp( - title: 'Stream Chat Dart Example', - home: HomeScreen(channel: channel), - ); + Widget build(BuildContext context) { + return MaterialApp( + title: 'Stream Chat Dart Example', + home: HomeScreen(channel: channel), + ); + } } /// Main screen of our application. The layout is comprised of an [AppBar] @@ -174,81 +176,83 @@ class _MessageViewState extends State { } @override - Widget build(BuildContext context) => Column( - children: [ - Expanded( - child: ListView.builder( - controller: _scrollController, - itemCount: _messages.length, - reverse: true, - itemBuilder: (BuildContext context, int index) { - final item = _messages[index]; - if (item.user?.id == widget.channel.client.uid) { - return Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.all(8), - child: Text(item.text ?? ''), - ), - ); - } else { - return Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.all(8), - child: Text(item.text ?? ''), - ), - ); - } - }, - ), + Widget build(BuildContext context) { + return Column( + children: [ + Expanded( + child: ListView.builder( + controller: _scrollController, + itemCount: _messages.length, + reverse: true, + itemBuilder: (BuildContext context, int index) { + final item = _messages[index]; + if (item.user?.id == widget.channel.client.uid) { + return Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.all(8), + child: Text(item.text ?? ''), + ), + ); + } else { + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(8), + child: Text(item.text ?? ''), + ), + ); + } + }, ), - Padding( - padding: const EdgeInsets.all(8), - child: Row( - children: [ - Expanded( - child: TextField( - controller: _controller, - decoration: const InputDecoration( - hintText: 'Enter your message', - ), + ), + Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _controller, + decoration: const InputDecoration( + hintText: 'Enter your message', ), ), - Material( - type: MaterialType.circle, - color: Colors.blue, - clipBehavior: Clip.hardEdge, - child: InkWell( - onTap: () async { - // We can send a new message by calling `sendMessage` on - // the current channel. After sending a message, the - // TextField is cleared and the list view is scrolled - // to show the new item. - if (_controller.value.text.isNotEmpty) { - await widget.channel.sendMessage( - Message(text: _controller.value.text), - ); - _controller.clear(); - _updateList(); - } - }, - child: const Padding( - padding: EdgeInsets.all(8), - child: Center( - child: Icon( - Icons.send, - color: Colors.white, - ), + ), + Material( + type: MaterialType.circle, + color: Colors.blue, + clipBehavior: Clip.hardEdge, + child: InkWell( + onTap: () async { + // We can send a new message by calling `sendMessage` on + // the current channel. After sending a message, the + // TextField is cleared and the list view is scrolled + // to show the new item. + if (_controller.value.text.isNotEmpty) { + await widget.channel.sendMessage( + Message(text: _controller.value.text), + ); + _controller.clear(); + _updateList(); + } + }, + child: const Padding( + padding: EdgeInsets.all(8), + child: Center( + child: Icon( + Icons.send, + color: Colors.white, ), ), ), ), - ], - ), + ), + ], ), - ], - ); + ), + ], + ); + } } /// Helper extension for quickly retrieving diff --git a/packages/stream_chat_persistence/example/linux/.gitignore b/packages/stream_chat_persistence/example/linux/.gitignore new file mode 100644 index 00000000..d3896c98 --- /dev/null +++ b/packages/stream_chat_persistence/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/packages/stream_chat_persistence/example/linux/CMakeLists.txt b/packages/stream_chat_persistence/example/linux/CMakeLists.txt new file mode 100644 index 00000000..a558bc45 --- /dev/null +++ b/packages/stream_chat_persistence/example/linux/CMakeLists.txt @@ -0,0 +1,116 @@ +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +set(BINARY_NAME "example") +set(APPLICATION_ID "com.example.example") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Configure build options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Application build +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) +apply_standard_settings(${BINARY_NAME}) +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) +add_dependencies(${BINARY_NAME} flutter_assemble) +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/packages/stream_chat_persistence/example/linux/flutter/CMakeLists.txt b/packages/stream_chat_persistence/example/linux/flutter/CMakeLists.txt new file mode 100644 index 00000000..33fd5801 --- /dev/null +++ b/packages/stream_chat_persistence/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,87 @@ +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/packages/stream_chat_persistence/example/linux/flutter/generated_plugin_registrant.cc b/packages/stream_chat_persistence/example/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..2c1ec4fe --- /dev/null +++ b/packages/stream_chat_persistence/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); + sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); +} diff --git a/packages/stream_chat_persistence/example/linux/flutter/generated_plugin_registrant.h b/packages/stream_chat_persistence/example/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..e0f0a47b --- /dev/null +++ b/packages/stream_chat_persistence/example/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/stream_chat_persistence/example/linux/flutter/generated_plugins.cmake b/packages/stream_chat_persistence/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 00000000..7ea2a801 --- /dev/null +++ b/packages/stream_chat_persistence/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + sqlite3_flutter_libs +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/stream_chat_persistence/example/linux/main.cc b/packages/stream_chat_persistence/example/linux/main.cc new file mode 100644 index 00000000..e7c5c543 --- /dev/null +++ b/packages/stream_chat_persistence/example/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/packages/stream_chat_persistence/example/linux/my_application.cc b/packages/stream_chat_persistence/example/linux/my_application.cc new file mode 100644 index 00000000..0ba8f430 --- /dev/null +++ b/packages/stream_chat_persistence/example/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/packages/stream_chat_persistence/example/linux/my_application.h b/packages/stream_chat_persistence/example/linux/my_application.h new file mode 100644 index 00000000..72271d5e --- /dev/null +++ b/packages/stream_chat_persistence/example/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/packages/stream_chat_persistence/example/macos/.gitignore b/packages/stream_chat_persistence/example/macos/.gitignore new file mode 100644 index 00000000..746adbb6 --- /dev/null +++ b/packages/stream_chat_persistence/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/packages/stream_chat_persistence/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/stream_chat_persistence/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 00000000..4b81f9b2 --- /dev/null +++ b/packages/stream_chat_persistence/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/stream_chat_persistence/example/macos/Flutter/Flutter-Release.xcconfig b/packages/stream_chat_persistence/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 00000000..5caa9d15 --- /dev/null +++ b/packages/stream_chat_persistence/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/stream_chat_persistence/example/macos/Runner.xcodeproj/project.pbxproj b/packages/stream_chat_persistence/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..4c2fe3c3 --- /dev/null +++ b/packages/stream_chat_persistence/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,632 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 2B7C563A37741A3C9EF92B31 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B1A5184EBFC1CF12D8F0FED /* Pods_Runner.framework */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0FF0B7F379F1614C9729FAA8 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 4A1304048C006F772B1A0FD8 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 67593663D5F7E4E722209E5F /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 7B1A5184EBFC1CF12D8F0FED /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2B7C563A37741A3C9EF92B31 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 920F14AC24D3A10BED75FEA8 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* example.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 920F14AC24D3A10BED75FEA8 /* Pods */ = { + isa = PBXGroup; + children = ( + 0FF0B7F379F1614C9729FAA8 /* Pods-Runner.debug.xcconfig */, + 4A1304048C006F772B1A0FD8 /* Pods-Runner.release.xcconfig */, + 67593663D5F7E4E722209E5F /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 7B1A5184EBFC1CF12D8F0FED /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + BEC283DD873799EEEA283BD1 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + B9255738A59BE48A11F25A1C /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + B9255738A59BE48A11F25A1C /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + BEC283DD873799EEEA283BD1 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/packages/stream_chat_persistence/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/stream_chat_persistence/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/packages/stream_chat_persistence/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/stream_chat_persistence/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/stream_chat_persistence/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..fb7259e1 --- /dev/null +++ b/packages/stream_chat_persistence/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/stream_chat_persistence/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/stream_chat_persistence/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/packages/stream_chat_persistence/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/stream_chat_persistence/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/stream_chat_persistence/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/packages/stream_chat_persistence/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/stream_chat_persistence/example/macos/Runner/AppDelegate.swift b/packages/stream_chat_persistence/example/macos/Runner/AppDelegate.swift new file mode 100644 index 00000000..d53ef643 --- /dev/null +++ b/packages/stream_chat_persistence/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..a2ec33f1 --- /dev/null +++ b/packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..3c4935a7ca84f0976aca34b7f2895d65fb94d1ea GIT binary patch literal 46993 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 literal 0 HcmV?d00001 diff --git a/packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..ed4cc16421680a50164ba74381b4b35ceaa0ccfc GIT binary patch literal 3276 zcmZ`*X*|?x8~)E?#xi3t91%vcMKbnsIy2_j%QE2ziLq8HEtbf{7%?Q-9a%z_Y^9`> zEHh*&vUG%uWkg7pKTS-`$veH@-Vg8ZdG7oAJ@<88AMX3Z{d}TU-4*=KI1-hF6u>DKF2moPt09c{` zfN3rO$X+gJI&oA$AbgKoTL8PiPI1eFOhHBDvW+$&oPl1s$+O5y3$30Jx9nC_?fg%8Om)@;^P;Ee~8ibejUNlSR{FL7-+ zCzU}3UT98m{kYI^@`mgCOJ))+D#erb#$UWt&((j-5*t1id2Zak{`aS^W*K5^gM02# zUAhZn-JAUK>i+SNuFbWWd*7n1^!}>7qZ1CqCl*T+WoAy&z9pm~0AUt1cCV24f z3M@&G~UKrjVHa zjcE@a`2;M>eV&ocly&W3h{`Kt`1Fpp?_h~9!Uj5>0eXw@$opV(@!pixIux}s5pvEqF5$OEMG0;c zAfMxC(-;nx_`}8!F?OqK19MeaswOomKeifCG-!9PiHSU$yamJhcjXiq)-}9`M<&Au|H!nKY(0`^x16f205i2i;E%(4!?0lLq0sH_%)Wzij)B{HZxYWRl3DLaN5`)L zx=x=|^RA?d*TRCwF%`zN6wn_1C4n;lZG(9kT;2Uhl&2jQYtC1TbwQlP^BZHY!MoHm zjQ9)uu_K)ObgvvPb}!SIXFCtN!-%sBQe{6NU=&AtZJS%}eE$i}FIll!r>~b$6gt)V z7x>OFE}YetHPc-tWeu!P@qIWb@Z$bd!*!*udxwO6&gJ)q24$RSU^2Mb%-_`dR2`nW z)}7_4=iR`Tp$TPfd+uieo)8B}Q9#?Szmy!`gcROB@NIehK|?!3`r^1>av?}e<$Qo` zo{Qn#X4ktRy<-+f#c@vILAm;*sfS}r(3rl+{op?Hx|~DU#qsDcQDTvP*!c>h*nXU6 zR=Un;i9D!LcnC(AQ$lTUv^pgv4Z`T@vRP3{&xb^drmjvOruIBJ%3rQAFLl7d9_S64 zN-Uv?R`EzkbYIo)af7_M=X$2p`!u?nr?XqQ_*F-@@(V zFbNeVEzbr;i2fefJ@Gir3-s`syC93he_krL1eb;r(}0yUkuEK34aYvC@(yGi`*oq? zw5g_abg=`5Fdh1Z+clSv*N*Jifmh&3Ghm0A=^s4be*z5N!i^FzLiShgkrkwsHfMjf z*7&-G@W>p6En#dk<^s@G?$7gi_l)y7k`ZY=?ThvvVKL~kM{ehG7-q6=#%Q8F&VsB* zeW^I zUq+tV(~D&Ii_=gn-2QbF3;Fx#%ajjgO05lfF8#kIllzHc=P}a3$S_XsuZI0?0__%O zjiL!@(C0$Nr+r$>bHk(_oc!BUz;)>Xm!s*C!32m1W<*z$^&xRwa+AaAG= z9t4X~7UJht1-z88yEKjJ68HSze5|nKKF9(Chw`{OoG{eG0mo`^93gaJmAP_i_jF8a z({|&fX70PXVE(#wb11j&g4f{_n>)wUYIY#vo>Rit(J=`A-NYYowTnl(N6&9XKIV(G z1aD!>hY!RCd^Sy#GL^0IgYF~)b-lczn+X}+eaa)%FFw41P#f8n2fm9=-4j7}ULi@Z zm=H8~9;)ShkOUAitb!1fvv%;2Q+o)<;_YA1O=??ie>JmIiTy6g+1B-1#A(NAr$JNL znVhfBc8=aoz&yqgrN|{VlpAniZVM?>0%bwB6>}S1n_OURps$}g1t%)YmCA6+5)W#B z=G^KX>C7x|X|$~;K;cc2x8RGO2{{zmjPFrfkr6AVEeW2$J9*~H-4~G&}~b+Pb}JJdODU|$n1<7GPa_>l>;{NmA^y_eXTiv z)T61teOA9Q$_5GEA_ox`1gjz>3lT2b?YY_0UJayin z64qq|Nb7^UhikaEz3M8BKhNDhLIf};)NMeS8(8?3U$ThSMIh0HG;;CW$lAp0db@s0 zu&jbmCCLGE*NktXVfP3NB;MQ>p?;*$-|htv>R`#4>OG<$_n)YvUN7bwzbWEsxAGF~ zn0Vfs?Dn4}Vd|Cf5T-#a52Knf0f*#2D4Lq>-Su4g`$q={+5L$Ta|N8yfZ}rgQm;&b z0A4?$Hg5UkzI)29=>XSzdH4wH8B@_KE{mSc>e3{yGbeiBY_+?^t_a#2^*x_AmN&J$ zf9@<5N15~ty+uwrz0g5k$sL9*mKQazK2h19UW~#H_X83ap-GAGf#8Q5b8n@B8N2HvTiZu&Mg+xhthyG3#0uIny33r?t&kzBuyI$igd`%RIcO8{s$$R3+Z zt{ENUO)pqm_&<(vPf*$q1FvC}W&G)HQOJd%x4PbxogX2a4eW-%KqA5+x#x`g)fN&@ zLjG8|!rCj3y0%N)NkbJVJgDu5tOdMWS|y|Tsb)Z04-oAVZ%Mb311P}}SG#!q_ffMV z@*L#25zW6Ho?-x~8pKw4u9X)qFI7TRC)LlEL6oQ9#!*0k{=p?Vf_^?4YR(M z`uD+8&I-M*`sz5af#gd$8rr|oRMVgeI~soPKB{Q{FwV-FW)>BlS?inI8girWs=mo5b18{#~CJz!miCgQYU>KtCPt()StN;x)c2P3bMVB$o(QUh z$cRQlo_?#k`7A{Tw z!~_YKSd(%1dBM+KE!5I2)ZZsGz|`+*fB*n}yxtKVyx14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>GbI`Jdw*pGcA%L+*Q#&*YQOJ$_%U#(BDn``;rKxi&&)LfRxIZ*98z8UWRslDo@Xu)QVh}rB>bKwe@Bjzwg%m$hd zG)gFMgHZlPxGcm3paLLb44yHI|Ag0wdp!_yD5R<|B29Ui~27`?vfy#ktk_KyHWMDA42{J=Uq-o}i z*%kZ@45mQ-Rw?0?K+z{&5KFc}xc5Q%1PFAbL_xCmpj?JNAm>L6SjrCMpiK}5LG0ZE zO>_%)r1c48n{Iv*t(u1=&kH zeO=ifbFy+6aSK)V_5t;NKhE#$Iz=+Oii|KDJ}W>g}0%`Svgra*tnS6TRU4iTH*e=dj~I` zym|EM*}I1?pT2#3`oZ(|3I-Y$DkeHMN=8~%YSR?;>=X?(Emci*ZIz9+t<|S1>hE8$ zVa1LmTh{DZv}x6@Wz!a}+qZDz%AHHMuHCzM^XlEpr!QPzf9QzkS_0!&1MPx*ICxe}RFdTH+c}l9E`G zYL#4+3Zxi}3=A!G4S>ir#L(2r)WFKnP}jiR%D`ZOPH`@ZhTQy=%(P0}8ZH)|z6jL7 N;OXk;vd$@?2>?>Ex^Vyi literal 0 HcmV?d00001 diff --git a/packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000000000000000000000000000000000000..bcbf36df2f2aaaa0a63c7dabc94e600184229d0d GIT binary patch literal 5933 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 f$KZHO*s&vt|9^w-6P?|#0pRK8NSwWJ?9znhg z3{`3j3=J&|48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!ItFh?!xdN1Q+aGJ{c&& zS>O>_%)r1c48n{Iv*t(u1=&kHeO=ifbFy+6aSK)V_AxLppYn8Z42d|rc6w}vOsL55 z`t&mC&y2@JTEyg!eDiFX^k#CC!jq%>erB=yHqUP0XcDOTw6ko}L zX;EmMrq(fKk*eygEuA616;0)>@A{TK|55PV@70 z$OfzS*(VJxQev3J?yY?O=ul(v`fp}?u9z`JK3ugibK>)DyCwImZOF4d{xK%%Ks1*} zv$oa)9anR%lXIBUqYnhLmT>VOzHfNP?ZwJNZ!5$s9M08RynIvaXw>@G^T9@r9^KH1 zVy??F&uuk)bH9Y4pQY!hP58i_H6 znl-NcuCpLV6ZWU;4C zu@9exF&OZi`Bovq_m%T+WhU2kvkz@^_LpycBvqm3bMpLw8X-Or5sL>0AKE1$(k_L=_Zc=CUq#=x1-QZf)G7nHu@fmsQ1eN_N3+nTEz`4HI4Z6uVlE zJH+X&det8JU?tO?upcM4Z=cV!JV;yF>FfL5Q$M|W_2Z!P`S=}Wzp|_1^#d%e?_H`> zV@%vA$+bFVqhw9`U;TfP|5|PD{||OiYdor8P*i??|NJcb%kzT_73*7WE?Ua5hAnR2 z=7WE=PhTlJ#ZeRznjTUb;`E(wkMZrj4e|Hilz-mK>9cZHQY**5TUPw~u}k;u73KI}xAx!0m-)GVia|x^d3p~s_9gh83jA&Ra<8rM%`>U3x69t&NzbwWY}7Ar?)FK#IZ0z|d0H0EkRO w3{9;}4Xg|ebq&m|3=9_N6z8I7$jwj5OsmAL;bP(Gi$Dzwp00i_>zopr02+f8CIA2c literal 0 HcmV?d00001 diff --git a/packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000000000000000000000000000000000000..e71a726136a47ed24125c7efc79d68a4a01961b4 GIT binary patch literal 14800 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 + + + + + + + + + + + + + + + + + + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/stream_chat_persistence/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/stream_chat_persistence/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 00000000..8b42559e --- /dev/null +++ b/packages/stream_chat_persistence/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.example + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2022 com.example. All rights reserved. diff --git a/packages/stream_chat_persistence/example/macos/Runner/Configs/Debug.xcconfig b/packages/stream_chat_persistence/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 00000000..36b0fd94 --- /dev/null +++ b/packages/stream_chat_persistence/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/stream_chat_persistence/example/macos/Runner/Configs/Release.xcconfig b/packages/stream_chat_persistence/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 00000000..dff4f495 --- /dev/null +++ b/packages/stream_chat_persistence/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/stream_chat_persistence/example/macos/Runner/Configs/Warnings.xcconfig b/packages/stream_chat_persistence/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 00000000..42bcbf47 --- /dev/null +++ b/packages/stream_chat_persistence/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/packages/stream_chat_persistence/example/macos/Runner/DebugProfile.entitlements b/packages/stream_chat_persistence/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 00000000..08c3ab17 --- /dev/null +++ b/packages/stream_chat_persistence/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + + diff --git a/packages/stream_chat_persistence/example/macos/Runner/Info.plist b/packages/stream_chat_persistence/example/macos/Runner/Info.plist new file mode 100644 index 00000000..4789daa6 --- /dev/null +++ b/packages/stream_chat_persistence/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/packages/stream_chat_persistence/example/macos/Runner/MainFlutterWindow.swift b/packages/stream_chat_persistence/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 00000000..2722837e --- /dev/null +++ b/packages/stream_chat_persistence/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/packages/stream_chat_persistence/example/macos/Runner/Release.entitlements b/packages/stream_chat_persistence/example/macos/Runner/Release.entitlements new file mode 100644 index 00000000..852fa1a4 --- /dev/null +++ b/packages/stream_chat_persistence/example/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/packages/stream_chat_persistence/example/pubspec.yaml b/packages/stream_chat_persistence/example/pubspec.yaml index 5043fa9f..2cc5d034 100644 --- a/packages/stream_chat_persistence/example/pubspec.yaml +++ b/packages/stream_chat_persistence/example/pubspec.yaml @@ -11,8 +11,8 @@ dependencies: cupertino_icons: ^1.0.3 flutter: sdk: flutter - stream_chat: ^4.6.0 - stream_chat_persistence: ^4.4.0 + stream_chat: ^5.0.0-beta.2 + stream_chat_persistence: ^5.0.0-beta.2 dev_dependencies: flutter_test: sdk: flutter diff --git a/packages/stream_chat_persistence/example/web/favicon.png b/packages/stream_chat_persistence/example/web/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8aaa46ac1ae21512746f852a42ba87e4165dfdd1 GIT binary patch literal 917 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0X7 zltGxWVyS%@P(fs7NJL45ua8x7ey(0(N`6wRUPW#JP&EUCO@$SZnVVXYs8ErclUHn2 zVXFjIVFhG^g!Ppaz)DK8ZIvQ?0~DO|i&7O#^-S~(l1AfjnEK zjFOT9D}DX)@^Za$W4-*MbbUihOG|wNBYh(yU7!lx;>x^|#0uTKVr7USFmqf|i<65o z3raHc^AtelCMM;Vme?vOfh>Xph&xL%(-1c06+^uR^q@XSM&D4+Kp$>4P^%3{)XKjo zGZknv$b36P8?Z_gF{nK@`XI}Z90TzwSQO}0J1!f2c(B=V`5aP@1P1a|PZ!4!3&Gl8 zTYqUsf!gYFyJnXpu0!n&N*SYAX-%d(5gVjrHJWqXQshj@!Zm{!01WsQrH~9=kTxW#6SvuapgMqt>$=j#%eyGrQzr zP{L-3gsMA^$I1&gsBAEL+vxi1*Igl=8#8`5?A-T5=z-sk46WA1IUT)AIZHx1rdUrf zVJrJn<74DDw`j)Ki#gt}mIT-Q`XRa2-jQXQoI%w`nb|XblvzK${ZzlV)m-XcwC(od z71_OEC5Bt9GEXosOXaPTYOia#R4ID2TiU~`zVMl08TV_C%DnU4^+HE>9(CE4D6?Fz oujB08i7adh9xk7*FX66dWH6F5TM;?E2b5PlUHx3vIVCg!0Dx9vYXATM literal 0 HcmV?d00001 diff --git a/packages/stream_chat_persistence/example/web/icons/Icon-192.png b/packages/stream_chat_persistence/example/web/icons/Icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..b749bfef07473333cf1dd31e9eed89862a5d52aa GIT binary patch literal 5292 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 literal 0 HcmV?d00001 diff --git a/packages/stream_chat_persistence/example/web/icons/Icon-512.png b/packages/stream_chat_persistence/example/web/icons/Icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..88cfd48dff1169879ba46840804b412fe02fefd6 GIT binary patch literal 8252 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 literal 0 HcmV?d00001 diff --git a/packages/stream_chat_persistence/example/web/icons/Icon-maskable-192.png b/packages/stream_chat_persistence/example/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000000000000000000000000000000000000..eb9b4d76e525556d5d89141648c724331630325d GIT binary patch literal 5594 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 X>c!)3PgKfL&lX^$Z>Cpu&6)6jvi^Z! literal 0 HcmV?d00001 diff --git a/packages/stream_chat_persistence/example/web/icons/Icon-maskable-512.png b/packages/stream_chat_persistence/example/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..d69c56691fbdb0b7efa65097c7cc1edac12a6d3e GIT binary patch literal 20998 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 MPgg&ebxsLQ025IeI{*Lx literal 0 HcmV?d00001 diff --git a/packages/stream_chat_persistence/example/web/index.html b/packages/stream_chat_persistence/example/web/index.html new file mode 100644 index 00000000..20772446 --- /dev/null +++ b/packages/stream_chat_persistence/example/web/index.html @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + example + + + + + + + + diff --git a/packages/stream_chat_persistence/example/web/manifest.json b/packages/stream_chat_persistence/example/web/manifest.json new file mode 100644 index 00000000..096edf8f --- /dev/null +++ b/packages/stream_chat_persistence/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/packages/stream_chat_persistence/example/web/sql-wasm.js b/packages/stream_chat_persistence/example/web/sql-wasm.js new file mode 100644 index 00000000..980a959a --- /dev/null +++ b/packages/stream_chat_persistence/example/web/sql-wasm.js @@ -0,0 +1,201 @@ + +// We are modularizing this manually because the current modularize setting in Emscripten has some issues: +// https://github.com/kripken/emscripten/issues/5820 +// In addition, When you use emcc's modularization, it still expects to export a global object called `Module`, +// which is able to be used/called before the WASM is loaded. +// The modularization below exports a promise that loads and resolves to the actual sql.js module. +// That way, this module can't be used before the WASM is finished loading. + +// We are going to define a function that a user will call to start loading initializing our Sql.js library +// However, that function might be called multiple times, and on subsequent calls, we don't actually want it to instantiate a new instance of the Module +// Instead, we want to return the previously loaded module + +// TODO: Make this not declare a global if used in the browser +var initSqlJsPromise = undefined; + +var initSqlJs = function (moduleConfig) { + + if (initSqlJsPromise){ + return initSqlJsPromise; + } + // If we're here, we've never called this function before + initSqlJsPromise = new Promise(function (resolveModule, reject) { + + // We are modularizing this manually because the current modularize setting in Emscripten has some issues: + // https://github.com/kripken/emscripten/issues/5820 + + // The way to affect the loading of emcc compiled modules is to create a variable called `Module` and add + // properties to it, like `preRun`, `postRun`, etc + // We are using that to get notified when the WASM has finished loading. + // Only then will we return our promise + + // If they passed in a moduleConfig object, use that + // Otherwise, initialize Module to the empty object + var Module = typeof moduleConfig !== 'undefined' ? moduleConfig : {}; + + // EMCC only allows for a single onAbort function (not an array of functions) + // So if the user defined their own onAbort function, we remember it and call it + var originalOnAbortFunction = Module['onAbort']; + Module['onAbort'] = function (errorThatCausedAbort) { + reject(new Error(errorThatCausedAbort)); + if (originalOnAbortFunction){ + originalOnAbortFunction(errorThatCausedAbort); + } + }; + + Module['postRun'] = Module['postRun'] || []; + Module['postRun'].push(function () { + // When Emscripted calls postRun, this promise resolves with the built Module + resolveModule(Module); + }); + + // There is a section of code in the emcc-generated code below that looks like this: + // (Note that this is lowercase `module`) + // if (typeof module !== 'undefined') { + // module['exports'] = Module; + // } + // When that runs, it's going to overwrite our own modularization export efforts in shell-post.js! + // The only way to tell emcc not to emit it is to pass the MODULARIZE=1 or MODULARIZE_INSTANCE=1 flags, + // but that carries with it additional unnecessary baggage/bugs we don't want either. + // So, we have three options: + // 1) We undefine `module` + // 2) We remember what `module['exports']` was at the beginning of this function and we restore it later + // 3) We write a script to remove those lines of code as part of the Make process. + // + // Since those are the only lines of code that care about module, we will undefine it. It's the most straightforward + // of the options, and has the side effect of reducing emcc's efforts to modify the module if its output were to change in the future. + // That's a nice side effect since we're handling the modularization efforts ourselves + module = undefined; + + // The emcc-generated code and shell-post.js code goes below, + // meaning that all of it runs inside of this promise. If anything throws an exception, our promise will abort + +var e;e||(e=typeof Module !== 'undefined' ? Module : {});null; +e.onRuntimeInitialized=function(){function a(h,l){this.Qa=h;this.db=l;this.Oa=1;this.kb=[]}function b(h,l){this.db=l;l=aa(h)+1;this.cb=da(l);if(null===this.cb)throw Error("Unable to allocate memory for the SQL string");k(h,n,this.cb,l);this.ib=this.cb;this.Za=this.ob=null}function c(h){this.filename="dbfile_"+(4294967295*Math.random()>>>0);if(null!=h){var l=this.filename,q=l?r("//"+l):"/";l=ea(!0,!0);q=fa(q,(void 0!==l?l:438)&4095|32768,0);if(h){if("string"===typeof h){for(var p=Array(h.length),z= +0,N=h.length;zc;++c)f.parameters.push(d["viii"[c]]); +c=new WebAssembly.Function(f,a)}else{d=[1,0,1,96];f={i:127,j:126,f:125,d:124};d.push(3);for(c=0;3>c;++c)d.push(f["iii"[c]]);d.push(0);d[1]=d.length-2;c=new Uint8Array([0,97,115,109,1,0,0,0].concat(d,[2,7,1,1,101,1,102,0,0,7,5,1,1,102,0,0]));c=new WebAssembly.Module(c);c=(new WebAssembly.Instance(c,{e:{f:a}})).exports.f}J.set(b,c)}Ja.set(a,b);a=b}return a}var Ka;e.wasmBinary&&(Ka=e.wasmBinary);var noExitRuntime=e.noExitRuntime||!0;"object"!==typeof WebAssembly&&F("no native wasm support detected"); +function qa(a){var b="i32";"*"===b.charAt(b.length-1)&&(b="i32");switch(b){case "i1":y[a>>0]=0;break;case "i8":y[a>>0]=0;break;case "i16":La[a>>1]=0;break;case "i32":K[a>>2]=0;break;case "i64":L=[0,(M=0,1<=+Math.abs(M)?0>>0:~~+Math.ceil((M-+(~~M>>>0))/4294967296)>>>0:0)];K[a>>2]=L[0];K[a+4>>2]=L[1];break;case "float":Ma[a>>2]=0;break;case "double":Na[a>>3]=0;break;default:F("invalid type for setValue: "+b)}} +function v(a,b){b=b||"i8";"*"===b.charAt(b.length-1)&&(b="i32");switch(b){case "i1":return y[a>>0];case "i8":return y[a>>0];case "i16":return La[a>>1];case "i32":return K[a>>2];case "i64":return K[a>>2];case "float":return Ma[a>>2];case "double":return Na[a>>3];default:F("invalid type for getValue: "+b)}return null}var Oa,Pa=!1;function Qa(a){var b=e["_"+a];b||F("Assertion failed: Cannot call unknown function "+(a+", make sure it is exported"));return b} +function Ra(a,b,c,d){var f={string:function(u){var C=0;if(null!==u&&void 0!==u&&0!==u){var I=(u.length<<2)+1;C=x(I);k(u,n,C,I)}return C},array:function(u){var C=x(u.length);y.set(u,C);return C}};a=Qa(a);var g=[],m=0;if(d)for(var t=0;t=d);)++c;if(16f?d+=String.fromCharCode(f):(f-=65536,d+=String.fromCharCode(55296|f>>10,56320|f&1023))}}else d+=String.fromCharCode(f)}return d}function A(a,b){return a?Va(n,a,b):""} +function k(a,b,c,d){if(!(0=m){var t=a.charCodeAt(++g);m=65536+((m&1023)<<10)|t&1023}if(127>=m){if(c>=d)break;b[c++]=m}else{if(2047>=m){if(c+1>=d)break;b[c++]=192|m>>6}else{if(65535>=m){if(c+2>=d)break;b[c++]=224|m>>12}else{if(c+3>=d)break;b[c++]=240|m>>18;b[c++]=128|m>>12&63}b[c++]=128|m>>6&63}b[c++]=128|m&63}}b[c]=0;return c-f} +function aa(a){for(var b=0,c=0;c=d&&(d=65536+((d&1023)<<10)|a.charCodeAt(++c)&1023);127>=d?++b:b=2047>=d?b+2:65535>=d?b+3:b+4}return b}function Wa(a){var b=aa(a)+1,c=da(b);c&&k(a,y,c,b);return c}var Xa,y,n,La,K,Ma,Na; +function Ya(){var a=Oa.buffer;Xa=a;e.HEAP8=y=new Int8Array(a);e.HEAP16=La=new Int16Array(a);e.HEAP32=K=new Int32Array(a);e.HEAPU8=n=new Uint8Array(a);e.HEAPU16=new Uint16Array(a);e.HEAPU32=new Uint32Array(a);e.HEAPF32=Ma=new Float32Array(a);e.HEAPF64=Na=new Float64Array(a)}var J,Za=[],$a=[],ab=[];function bb(){var a=e.preRun.shift();Za.unshift(a)}var cb=0,db=null,eb=null;e.preloadedImages={};e.preloadedAudios={}; +function F(a){if(e.onAbort)e.onAbort(a);H(a);Pa=!0;throw new WebAssembly.RuntimeError("abort("+a+"). Build with -s ASSERTIONS=1 for more info.");}function fb(){return P.startsWith("data:application/octet-stream;base64,")}var P;P="sql-wasm.wasm";if(!fb()){var gb=P;P=e.locateFile?e.locateFile(gb,E):E+gb}function hb(){var a=P;try{if(a==P&&Ka)return new Uint8Array(Ka);if(Ea)return Ea(a);throw"both async and sync fetching of the wasm failed";}catch(b){F(b)}} +function ib(){if(!Ka&&(ya||za)){if("function"===typeof fetch&&!P.startsWith("file://"))return fetch(P,{credentials:"same-origin"}).then(function(a){if(!a.ok)throw"failed to load wasm binary file at '"+P+"'";return a.arrayBuffer()}).catch(function(){return hb()});if(Da)return new Promise(function(a,b){Da(P,function(c){a(new Uint8Array(c))},b)})}return Promise.resolve().then(function(){return hb()})}var M,L; +function jb(a){for(;0>2]=60*g;K[nb()>>2]=Number(b!=f);c=a(c);d=a(d);c=Wa(c);d=Wa(d);f>2]=c,K[ob()+4>>2]=d):(K[ob()>>2]=d,K[ob()+4>>2]=c)}var pb; +function ub(a,b){for(var c=0,d=a.length-1;0<=d;d--){var f=a[d];"."===f?a.splice(d,1):".."===f?(a.splice(d,1),c++):c&&(a.splice(d,1),c--)}if(b)for(;c;c--)a.unshift("..");return a}function r(a){var b="/"===a.charAt(0),c="/"===a.substr(-1);(a=ub(a.split("/").filter(function(d){return!!d}),!b).join("/"))||b||(a=".");a&&c&&(a+="/");return(b?"/":"")+a} +function vb(a){var b=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/.exec(a).slice(1);a=b[0];b=b[1];if(!a&&!b)return".";b&&(b=b.substr(0,b.length-1));return a+b}function xb(a){if("/"===a)return"/";a=r(a);a=a.replace(/\/$/,"");var b=a.lastIndexOf("/");return-1===b?a:a.substr(b+1)} +function yb(){if("object"===typeof crypto&&"function"===typeof crypto.getRandomValues){var a=new Uint8Array(1);return function(){crypto.getRandomValues(a);return a[0]}}if(Ba)try{var b=require("crypto");return function(){return b.randomBytes(1)[0]}}catch(c){}return function(){F("randomDevice")}} +function zb(){for(var a="",b=!1,c=arguments.length-1;-1<=c&&!b;c--){b=0<=c?arguments[c]:"/";if("string"!==typeof b)throw new TypeError("Arguments to path.resolve must be strings");if(!b)return"";a=b+"/"+a;b="/"===b.charAt(0)}a=ub(a.split("/").filter(function(d){return!!d}),!b).join("/");return(b?"/":"")+a||"."}var Ab=[];function Bb(a,b){Ab[a]={input:[],output:[],bb:b};Cb(a,Db)} +var Db={open:function(a){var b=Ab[a.node.rdev];if(!b)throw new Q(43);a.tty=b;a.seekable=!1},close:function(a){a.tty.bb.flush(a.tty)},flush:function(a){a.tty.bb.flush(a.tty)},read:function(a,b,c,d){if(!a.tty||!a.tty.bb.zb)throw new Q(60);for(var f=0,g=0;g=b||(b=Math.max(b,c*(1048576>c?2:1.125)>>>0),0!=c&&(b=Math.max(b,256)),c=a.Na,a.Na=new Uint8Array(b),0=a.node.Ra)return 0;a=Math.min(a.node.Ra-f,d);if(8b)throw new Q(28);return b},rb:function(a,b,c){R.wb(a.node,b+c);a.node.Ra=Math.max(a.node.Ra,b+c)},gb:function(a,b,c,d,f,g){if(0!==b)throw new Q(28);if(32768!==(a.node.mode&61440))throw new Q(43);a=a.node.Na;if(g& +2||a.buffer!==Xa){if(0>>0)%U.length}function Tb(a){var b=Sb(a.parent.id,a.name);if(U[b]===a)U[b]=a.ab;else for(b=U[b];b;){if(b.ab===a){b.ab=a.ab;break}b=b.ab}} +function Lb(a,b){var c;if(c=(c=Ub(a,"x"))?c:a.La.lookup?0:2)throw new Q(c,a);for(c=U[Sb(a.id,b)];c;c=c.ab){var d=c.name;if(c.parent.id===a.id&&d===b)return c}return a.La.lookup(a,b)}function Jb(a,b,c,d){a=new Vb(a,b,c,d);b=Sb(a.parent.id,a.name);a.ab=U[b];return U[b]=a}function S(a){return 16384===(a&61440)}var Wb={r:0,"r+":2,w:577,"w+":578,a:1089,"a+":1090};function Xb(a){var b=["r","w","rw"][a&3];a&512&&(b+="w");return b} +function Ub(a,b){if(Pb)return 0;if(!b.includes("r")||a.mode&292){if(b.includes("w")&&!(a.mode&146)||b.includes("x")&&!(a.mode&73))return 2}else return 2;return 0}function Yb(a,b){try{return Lb(a,b),20}catch(c){}return Ub(a,"wx")}function Zb(a,b,c){try{var d=Lb(a,b)}catch(f){return f.Pa}if(a=Ub(a,"wx"))return a;if(c){if(!S(d.mode))return 54;if(d===d.parent||"/"===Rb(d))return 10}else if(S(d.mode))return 31;return 0}function $b(a){var b=4096;for(a=a||0;a<=b;a++)if(!T[a])return a;throw new Q(33);} +function ac(a,b){bc||(bc=function(){},bc.prototype={});var c=new bc,d;for(d in a)c[d]=a[d];a=c;b=$b(b);a.fd=b;return T[b]=a}var Ib={open:function(a){a.Ma=Nb[a.node.rdev].Ma;a.Ma.open&&a.Ma.open(a)},Ya:function(){throw new Q(70);}};function Cb(a,b){Nb[a]={Ma:b}} +function cc(a,b){var c="/"===b,d=!b;if(c&&Mb)throw new Q(10);if(!c&&!d){var f=W(b,{xb:!1});b=f.path;f=f.node;if(f.$a)throw new Q(10);if(!S(f.mode))throw new Q(54);}b={type:a,Sb:{},Ab:b,Kb:[]};a=a.Va(b);a.Va=b;b.root=a;c?Mb=a:f&&(f.$a=b,f.Va&&f.Va.Kb.push(b))}function fa(a,b,c){var d=W(a,{parent:!0}).node;a=xb(a);if(!a||"."===a||".."===a)throw new Q(28);var f=Yb(d,a);if(f)throw new Q(f);if(!d.La.fb)throw new Q(63);return d.La.fb(d,a,b,c)} +function X(a,b){return fa(a,(void 0!==b?b:511)&1023|16384,0)}function dc(a,b,c){"undefined"===typeof c&&(c=b,b=438);fa(a,b|8192,c)}function ec(a,b){if(!zb(a))throw new Q(44);var c=W(b,{parent:!0}).node;if(!c)throw new Q(44);b=xb(b);var d=Yb(c,b);if(d)throw new Q(d);if(!c.La.symlink)throw new Q(63);c.La.symlink(c,b,a)} +function ua(a){var b=W(a,{parent:!0}).node,c=xb(a),d=Lb(b,c),f=Zb(b,c,!1);if(f)throw new Q(f);if(!b.La.unlink)throw new Q(63);if(d.$a)throw new Q(10);try{V.willDeletePath&&V.willDeletePath(a)}catch(g){H("FS.trackingDelegate['willDeletePath']('"+a+"') threw an exception: "+g.message)}b.La.unlink(b,c);Tb(d);try{if(V.onDeletePath)V.onDeletePath(a)}catch(g){H("FS.trackingDelegate['onDeletePath']('"+a+"') threw an exception: "+g.message)}} +function Qb(a){a=W(a).node;if(!a)throw new Q(44);if(!a.La.readlink)throw new Q(28);return zb(Rb(a.parent),a.La.readlink(a))}function fc(a,b){a=W(a,{Xa:!b}).node;if(!a)throw new Q(44);if(!a.La.Ta)throw new Q(63);return a.La.Ta(a)}function gc(a){return fc(a,!0)}function ha(a,b){a="string"===typeof a?W(a,{Xa:!0}).node:a;if(!a.La.Sa)throw new Q(63);a.La.Sa(a,{mode:b&4095|a.mode&-4096,timestamp:Date.now()})} +function Ic(a){a="string"===typeof a?W(a,{Xa:!0}).node:a;if(!a.La.Sa)throw new Q(63);a.La.Sa(a,{timestamp:Date.now()})}function Jc(a,b){if(0>b)throw new Q(28);a="string"===typeof a?W(a,{Xa:!0}).node:a;if(!a.La.Sa)throw new Q(63);if(S(a.mode))throw new Q(31);if(32768!==(a.mode&61440))throw new Q(28);var c=Ub(a,"w");if(c)throw new Q(c);a.La.Sa(a,{size:b,timestamp:Date.now()})} +function ia(a,b,c,d){if(""===a)throw new Q(44);if("string"===typeof b){var f=Wb[b];if("undefined"===typeof f)throw Error("Unknown file open mode: "+b);b=f}c=b&64?("undefined"===typeof c?438:c)&4095|32768:0;if("object"===typeof a)var g=a;else{a=r(a);try{g=W(a,{Xa:!(b&131072)}).node}catch(m){}}f=!1;if(b&64)if(g){if(b&128)throw new Q(20);}else g=fa(a,c,0),f=!0;if(!g)throw new Q(44);8192===(g.mode&61440)&&(b&=-513);if(b&65536&&!S(g.mode))throw new Q(54);if(!f&&(c=g?40960===(g.mode&61440)?32:S(g.mode)&& +("r"!==Xb(b)||b&512)?31:Ub(g,Xb(b)):44))throw new Q(c);b&512&&Jc(g,0);b&=-131713;d=ac({node:g,path:Rb(g),flags:b,seekable:!0,position:0,Ma:g.Ma,Pb:[],error:!1},d);d.Ma.open&&d.Ma.open(d);!e.logReadFiles||b&1||(Lc||(Lc={}),a in Lc||(Lc[a]=1,H("FS.trackingDelegate error on read file: "+a)));try{V.onOpenFile&&(g=0,1!==(b&2097155)&&(g|=1),0!==(b&2097155)&&(g|=2),V.onOpenFile(a,g))}catch(m){H("FS.trackingDelegate['onOpenFile']('"+a+"', flags) threw an exception: "+m.message)}return d} +function la(a){if(null===a.fd)throw new Q(8);a.nb&&(a.nb=null);try{a.Ma.close&&a.Ma.close(a)}catch(b){throw b;}finally{T[a.fd]=null}a.fd=null}function Mc(a,b,c){if(null===a.fd)throw new Q(8);if(!a.seekable||!a.Ma.Ya)throw new Q(70);if(0!=c&&1!=c&&2!=c)throw new Q(28);a.position=a.Ma.Ya(a,b,c);a.Pb=[]} +function Nc(a,b,c,d,f){if(0>d||0>f)throw new Q(28);if(null===a.fd)throw new Q(8);if(1===(a.flags&2097155))throw new Q(8);if(S(a.node.mode))throw new Q(31);if(!a.Ma.read)throw new Q(28);var g="undefined"!==typeof f;if(!g)f=a.position;else if(!a.seekable)throw new Q(70);b=a.Ma.read(a,b,c,d,f);g||(a.position+=b);return b} +function ka(a,b,c,d,f,g){if(0>d||0>f)throw new Q(28);if(null===a.fd)throw new Q(8);if(0===(a.flags&2097155))throw new Q(8);if(S(a.node.mode))throw new Q(31);if(!a.Ma.write)throw new Q(28);a.seekable&&a.flags&1024&&Mc(a,0,2);var m="undefined"!==typeof f;if(!m)f=a.position;else if(!a.seekable)throw new Q(70);b=a.Ma.write(a,b,c,d,f,g);m||(a.position+=b);try{if(a.path&&V.onWriteToFile)V.onWriteToFile(a.path)}catch(t){H("FS.trackingDelegate['onWriteToFile']('"+a.path+"') threw an exception: "+t.message)}return b} +function ta(a){var b={encoding:"binary"};b=b||{};b.flags=b.flags||0;b.encoding=b.encoding||"binary";if("utf8"!==b.encoding&&"binary"!==b.encoding)throw Error('Invalid encoding type "'+b.encoding+'"');var c,d=ia(a,b.flags);a=fc(a).size;var f=new Uint8Array(a);Nc(d,f,0,a,0);"utf8"===b.encoding?c=Va(f,0):"binary"===b.encoding&&(c=f);la(d);return c} +function Oc(){Q||(Q=function(a,b){this.node=b;this.Ob=function(c){this.Pa=c};this.Ob(a);this.message="FS error"},Q.prototype=Error(),Q.prototype.constructor=Q,[44].forEach(function(a){Kb[a]=new Q(a);Kb[a].stack=""}))}var Pc;function ea(a,b){var c=0;a&&(c|=365);b&&(c|=146);return c} +function Qc(a,b,c){a=r("/dev/"+a);var d=ea(!!b,!!c);Rc||(Rc=64);var f=Rc++<<8|0;Cb(f,{open:function(g){g.seekable=!1},close:function(){c&&c.buffer&&c.buffer.length&&c(10)},read:function(g,m,t,w){for(var u=0,C=0;C>2]=d.dev;K[c+4>>2]=0;K[c+8>>2]=d.ino;K[c+12>>2]=d.mode;K[c+16>>2]=d.nlink;K[c+20>>2]=d.uid;K[c+24>>2]=d.gid;K[c+28>>2]=d.rdev;K[c+32>>2]=0;L=[d.size>>>0,(M=d.size,1<=+Math.abs(M)?0>>0:~~+Math.ceil((M-+(~~M>>>0))/4294967296)>>>0:0)];K[c+40>>2]=L[0];K[c+44>>2]=L[1];K[c+48>>2]=4096;K[c+52>>2]=d.blocks;K[c+56>>2]=d.atime.getTime()/1E3|0;K[c+60>>2]= +0;K[c+64>>2]=d.mtime.getTime()/1E3|0;K[c+68>>2]=0;K[c+72>>2]=d.ctime.getTime()/1E3|0;K[c+76>>2]=0;L=[d.ino>>>0,(M=d.ino,1<=+Math.abs(M)?0>>0:~~+Math.ceil((M-+(~~M>>>0))/4294967296)>>>0:0)];K[c+80>>2]=L[0];K[c+84>>2]=L[1];return 0}var Uc=void 0;function Vc(){Uc+=4;return K[Uc-4>>2]}function Z(a){a=T[a];if(!a)throw new Q(8);return a}var Wc;Wc=Ba?function(){var a=process.hrtime();return 1E3*a[0]+a[1]/1E6}:function(){return performance.now()}; +var Xc={};function Yc(){if(!Zc){var a={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:("object"===typeof navigator&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8",_:xa||"./this.program"},b;for(b in Xc)void 0===Xc[b]?delete a[b]:a[b]=Xc[b];var c=[];for(b in a)c.push(b+"="+a[b]);Zc=c}return Zc}var Zc; +function Vb(a,b,c,d){a||(a=this);this.parent=a;this.Va=a.Va;this.$a=null;this.id=Ob++;this.name=b;this.mode=c;this.La={};this.Ma={};this.rdev=d}Object.defineProperties(Vb.prototype,{read:{get:function(){return 365===(this.mode&365)},set:function(a){a?this.mode|=365:this.mode&=-366}},write:{get:function(){return 146===(this.mode&146)},set:function(a){a?this.mode|=146:this.mode&=-147}}});Oc();U=Array(4096);cc(R,"/");X("/tmp");X("/home");X("/home/web_user"); +(function(){X("/dev");Cb(259,{read:function(){return 0},write:function(b,c,d,f){return f}});dc("/dev/null",259);Bb(1280,Eb);Bb(1536,Fb);dc("/dev/tty",1280);dc("/dev/tty1",1536);var a=yb();Qc("random",a);Qc("urandom",a);X("/dev/shm");X("/dev/shm/tmp")})(); +(function(){X("/proc");var a=X("/proc/self");X("/proc/self/fd");cc({Va:function(){var b=Jb(a,"fd",16895,73);b.La={lookup:function(c,d){var f=T[+d];if(!f)throw new Q(8);c={parent:null,Va:{Ab:"fake"},La:{readlink:function(){return f.path}}};return c.parent=c}};return b}},"/proc/self/fd")})();function ma(a,b){var c=Array(aa(a)+1);a=k(a,c,0,c.length);b&&(c.length=a);return c} +var ad={a:function(a,b,c,d){F("Assertion failed: "+A(a)+", at: "+[b?A(b):"unknown filename",c,d?A(d):"unknown function"])},s:function(a,b){pb||(pb=!0,lb());a=new Date(1E3*K[a>>2]);K[b>>2]=a.getSeconds();K[b+4>>2]=a.getMinutes();K[b+8>>2]=a.getHours();K[b+12>>2]=a.getDate();K[b+16>>2]=a.getMonth();K[b+20>>2]=a.getFullYear()-1900;K[b+24>>2]=a.getDay();var c=new Date(a.getFullYear(),0,1);K[b+28>>2]=(a.getTime()-c.getTime())/864E5|0;K[b+36>>2]=-(60*a.getTimezoneOffset());var d=(new Date(a.getFullYear(), +6,1)).getTimezoneOffset();c=c.getTimezoneOffset();a=(d!=c&&a.getTimezoneOffset()==Math.min(c,d))|0;K[b+32>>2]=a;a=K[ob()+(a?4:0)>>2];K[b+40>>2]=a;return b},y:function(a,b){try{a=A(a);if(b&-8)var c=-28;else{var d;(d=W(a,{Xa:!0}).node)?(a="",b&4&&(a+="r"),b&2&&(a+="w"),b&1&&(a+="x"),c=a&&Ub(d,a)?-2:0):c=-44}return c}catch(f){return"undefined"!==typeof Y&&f instanceof Q||F(f),-f.Pa}},i:function(a,b){try{return a=A(a),ha(a,b),0}catch(c){return"undefined"!==typeof Y&&c instanceof Q||F(c),-c.Pa}},z:function(a){try{return a= +A(a),Ic(a),0}catch(b){return"undefined"!==typeof Y&&b instanceof Q||F(b),-b.Pa}},j:function(a,b){try{var c=T[a];if(!c)throw new Q(8);ha(c.node,b);return 0}catch(d){return"undefined"!==typeof Y&&d instanceof Q||F(d),-d.Pa}},A:function(a){try{var b=T[a];if(!b)throw new Q(8);Ic(b.node);return 0}catch(c){return"undefined"!==typeof Y&&c instanceof Q||F(c),-c.Pa}},b:function(a,b,c){Uc=c;try{var d=Z(a);switch(b){case 0:var f=Vc();return 0>f?-28:ia(d.path,d.flags,0,f).fd;case 1:case 2:return 0;case 3:return d.flags; +case 4:return f=Vc(),d.flags|=f,0;case 12:return f=Vc(),La[f+0>>1]=2,0;case 13:case 14:return 0;case 16:case 8:return-28;case 9:return K[$c()>>2]=28,-1;default:return-28}}catch(g){return"undefined"!==typeof Y&&g instanceof Q||F(g),-g.Pa}},k:function(a,b){try{var c=Z(a);return Tc(fc,c.path,b)}catch(d){return"undefined"!==typeof Y&&d instanceof Q||F(d),-d.Pa}},E:function(a,b,c){try{var d=T[a];if(!d)throw new Q(8);if(0===(d.flags&2097155))throw new Q(28);Jc(d.node,c);return 0}catch(f){return"undefined"!== +typeof Y&&f instanceof Q||F(f),-f.Pa}},D:function(a,b){try{if(0===b)return-28;if(b=c)var d=-28;else{var f=Qb(a),g=Math.min(c,aa(f)), +m=y[b+g];k(f,n,b,c+1);y[b+g]=m;d=g}return d}catch(t){return"undefined"!==typeof Y&&t instanceof Q||F(t),-t.Pa}},H:function(a){try{a=A(a);var b=W(a,{parent:!0}).node,c=xb(a),d=Lb(b,c),f=Zb(b,c,!0);if(f)throw new Q(f);if(!b.La.rmdir)throw new Q(63);if(d.$a)throw new Q(10);try{V.willDeletePath&&V.willDeletePath(a)}catch(g){H("FS.trackingDelegate['willDeletePath']('"+a+"') threw an exception: "+g.message)}b.La.rmdir(b,c);Tb(d);try{if(V.onDeletePath)V.onDeletePath(a)}catch(g){H("FS.trackingDelegate['onDeletePath']('"+ +a+"') threw an exception: "+g.message)}return 0}catch(g){return"undefined"!==typeof Y&&g instanceof Q||F(g),-g.Pa}},e:function(a,b){try{return a=A(a),Tc(fc,a,b)}catch(c){return"undefined"!==typeof Y&&c instanceof Q||F(c),-c.Pa}},x:function(a){try{return a=A(a),ua(a),0}catch(b){return"undefined"!==typeof Y&&b instanceof Q||F(b),-b.Pa}},J:function(){return 2147483648},n:function(a,b,c){n.copyWithin(a,b,b+c)},c:function(a){var b=n.length;a>>>=0;if(2147483648=c;c*=2){var d=b* +(1+.2/c);d=Math.min(d,a+100663296);d=Math.max(a,d);0>>16);Ya();var f=1;break a}catch(g){}f=void 0}if(f)return!0}return!1},r:function(a){for(var b=Wc();Wc()-b>2]=g;for(g=0;g>0]=d.charCodeAt(g);y[f>>0]=0;c+=d.length+1});return 0},q:function(a,b){var c=Yc();K[a>>2]=c.length;var d=0;c.forEach(function(f){d+=f.length+1}); +K[b>>2]=d;return 0},f:function(a){try{var b=Z(a);la(b);return 0}catch(c){return"undefined"!==typeof Y&&c instanceof Q||F(c),c.Pa}},o:function(a,b){try{var c=Z(a);y[b>>0]=c.tty?2:S(c.mode)?3:40960===(c.mode&61440)?7:4;return 0}catch(d){return"undefined"!==typeof Y&&d instanceof Q||F(d),d.Pa}},w:function(a,b,c,d){try{a:{for(var f=Z(a),g=a=0;g>2],t=Nc(f,y,K[b+8*g>>2],m,void 0);if(0>t){var w=-1;break a}a+=t;if(t>2]=w;return 0}catch(u){return"undefined"!==typeof Y&& +u instanceof Q||F(u),u.Pa}},m:function(a,b,c,d,f){try{var g=Z(a);a=4294967296*c+(b>>>0);if(-9007199254740992>=a||9007199254740992<=a)return-61;Mc(g,a,d);L=[g.position>>>0,(M=g.position,1<=+Math.abs(M)?0>>0:~~+Math.ceil((M-+(~~M>>>0))/4294967296)>>>0:0)];K[f>>2]=L[0];K[f+4>>2]=L[1];g.nb&&0===a&&0===d&&(g.nb=null);return 0}catch(m){return"undefined"!==typeof Y&&m instanceof Q||F(m),m.Pa}},G:function(a){try{var b=Z(a);return b.Ma&&b.Ma.fsync?-b.Ma.fsync(b): +0}catch(c){return"undefined"!==typeof Y&&c instanceof Q||F(c),c.Pa}},B:function(a,b,c,d){try{a:{for(var f=Z(a),g=a=0;g>2],K[b+(8*g+4)>>2],void 0);if(0>m){var t=-1;break a}a+=m}t=a}K[d>>2]=t;return 0}catch(w){return"undefined"!==typeof Y&&w instanceof Q||F(w),w.Pa}},g:function(a){var b=Date.now();K[a>>2]=b/1E3|0;K[a+4>>2]=b%1E3*1E3|0;return 0},K:function(a){var b=Date.now()/1E3|0;a&&(K[a>>2]=b);return b},C:function(a,b){if(b){var c=b+8;b=1E3*K[c>>2];b+=K[c+4>>2]/1E3}else b= +Date.now();a=A(a);try{var d=W(a,{Xa:!0}).node;d.La.Sa(d,{timestamp:Math.max(b,b)});var f=0}catch(g){if(!(g instanceof Q)){b:{f=Error();if(!f.stack){try{throw Error();}catch(m){f=m}if(!f.stack){f="(no stack trace available)";break b}}f=f.stack.toString()}e.extraStackTrace&&(f+="\n"+e.extraStackTrace());f=kb(f);throw g+" : "+f;}f=g.Pa;K[$c()>>2]=f;f=-1}return f}}; +(function(){function a(f){e.asm=f.exports;Oa=e.asm.L;Ya();J=e.asm.Ca;$a.unshift(e.asm.M);cb--;e.monitorRunDependencies&&e.monitorRunDependencies(cb);0==cb&&(null!==db&&(clearInterval(db),db=null),eb&&(f=eb,eb=null,f()))}function b(f){a(f.instance)}function c(f){return ib().then(function(g){return WebAssembly.instantiate(g,d)}).then(function(g){return g}).then(f,function(g){H("failed to asynchronously prepare wasm: "+g);F(g)})}var d={a:ad};cb++;e.monitorRunDependencies&&e.monitorRunDependencies(cb); +if(e.instantiateWasm)try{return e.instantiateWasm(d,a)}catch(f){return H("Module.instantiateWasm callback failed with error: "+f),!1}(function(){return Ka||"function"!==typeof WebAssembly.instantiateStreaming||fb()||P.startsWith("file://")||"function"!==typeof fetch?c(b):fetch(P,{credentials:"same-origin"}).then(function(f){return WebAssembly.instantiateStreaming(f,d).then(b,function(g){H("wasm streaming compile failed: "+g);H("falling back to ArrayBuffer instantiation");return c(b)})})})();return{}})(); +e.___wasm_call_ctors=function(){return(e.___wasm_call_ctors=e.asm.M).apply(null,arguments)};e._sqlite3_free=function(){return(e._sqlite3_free=e.asm.N).apply(null,arguments)};var $c=e.___errno_location=function(){return($c=e.___errno_location=e.asm.O).apply(null,arguments)};e._sqlite3_step=function(){return(e._sqlite3_step=e.asm.P).apply(null,arguments)};e._sqlite3_finalize=function(){return(e._sqlite3_finalize=e.asm.Q).apply(null,arguments)}; +e._sqlite3_prepare_v2=function(){return(e._sqlite3_prepare_v2=e.asm.R).apply(null,arguments)};e._sqlite3_reset=function(){return(e._sqlite3_reset=e.asm.S).apply(null,arguments)};e._sqlite3_clear_bindings=function(){return(e._sqlite3_clear_bindings=e.asm.T).apply(null,arguments)};e._sqlite3_value_blob=function(){return(e._sqlite3_value_blob=e.asm.U).apply(null,arguments)};e._sqlite3_value_text=function(){return(e._sqlite3_value_text=e.asm.V).apply(null,arguments)}; +e._sqlite3_value_bytes=function(){return(e._sqlite3_value_bytes=e.asm.W).apply(null,arguments)};e._sqlite3_value_double=function(){return(e._sqlite3_value_double=e.asm.X).apply(null,arguments)};e._sqlite3_value_int=function(){return(e._sqlite3_value_int=e.asm.Y).apply(null,arguments)};e._sqlite3_value_type=function(){return(e._sqlite3_value_type=e.asm.Z).apply(null,arguments)};e._sqlite3_result_blob=function(){return(e._sqlite3_result_blob=e.asm._).apply(null,arguments)}; +e._sqlite3_result_double=function(){return(e._sqlite3_result_double=e.asm.$).apply(null,arguments)};e._sqlite3_result_error=function(){return(e._sqlite3_result_error=e.asm.aa).apply(null,arguments)};e._sqlite3_result_int=function(){return(e._sqlite3_result_int=e.asm.ba).apply(null,arguments)};e._sqlite3_result_int64=function(){return(e._sqlite3_result_int64=e.asm.ca).apply(null,arguments)};e._sqlite3_result_null=function(){return(e._sqlite3_result_null=e.asm.da).apply(null,arguments)}; +e._sqlite3_result_text=function(){return(e._sqlite3_result_text=e.asm.ea).apply(null,arguments)};e._sqlite3_column_count=function(){return(e._sqlite3_column_count=e.asm.fa).apply(null,arguments)};e._sqlite3_data_count=function(){return(e._sqlite3_data_count=e.asm.ga).apply(null,arguments)};e._sqlite3_column_blob=function(){return(e._sqlite3_column_blob=e.asm.ha).apply(null,arguments)};e._sqlite3_column_bytes=function(){return(e._sqlite3_column_bytes=e.asm.ia).apply(null,arguments)}; +e._sqlite3_column_double=function(){return(e._sqlite3_column_double=e.asm.ja).apply(null,arguments)};e._sqlite3_column_text=function(){return(e._sqlite3_column_text=e.asm.ka).apply(null,arguments)};e._sqlite3_column_type=function(){return(e._sqlite3_column_type=e.asm.la).apply(null,arguments)};e._sqlite3_column_name=function(){return(e._sqlite3_column_name=e.asm.ma).apply(null,arguments)};e._sqlite3_bind_blob=function(){return(e._sqlite3_bind_blob=e.asm.na).apply(null,arguments)}; +e._sqlite3_bind_double=function(){return(e._sqlite3_bind_double=e.asm.oa).apply(null,arguments)};e._sqlite3_bind_int=function(){return(e._sqlite3_bind_int=e.asm.pa).apply(null,arguments)};e._sqlite3_bind_text=function(){return(e._sqlite3_bind_text=e.asm.qa).apply(null,arguments)};e._sqlite3_bind_parameter_index=function(){return(e._sqlite3_bind_parameter_index=e.asm.ra).apply(null,arguments)};e._sqlite3_sql=function(){return(e._sqlite3_sql=e.asm.sa).apply(null,arguments)}; +e._sqlite3_normalized_sql=function(){return(e._sqlite3_normalized_sql=e.asm.ta).apply(null,arguments)};e._sqlite3_errmsg=function(){return(e._sqlite3_errmsg=e.asm.ua).apply(null,arguments)};e._sqlite3_exec=function(){return(e._sqlite3_exec=e.asm.va).apply(null,arguments)};e._sqlite3_changes=function(){return(e._sqlite3_changes=e.asm.wa).apply(null,arguments)};e._sqlite3_close_v2=function(){return(e._sqlite3_close_v2=e.asm.xa).apply(null,arguments)}; +e._sqlite3_create_function_v2=function(){return(e._sqlite3_create_function_v2=e.asm.ya).apply(null,arguments)};e._sqlite3_open=function(){return(e._sqlite3_open=e.asm.za).apply(null,arguments)};var da=e._malloc=function(){return(da=e._malloc=e.asm.Aa).apply(null,arguments)},oa=e._free=function(){return(oa=e._free=e.asm.Ba).apply(null,arguments)};e._RegisterExtensionFunctions=function(){return(e._RegisterExtensionFunctions=e.asm.Da).apply(null,arguments)}; +var ob=e.__get_tzname=function(){return(ob=e.__get_tzname=e.asm.Ea).apply(null,arguments)},nb=e.__get_daylight=function(){return(nb=e.__get_daylight=e.asm.Fa).apply(null,arguments)},mb=e.__get_timezone=function(){return(mb=e.__get_timezone=e.asm.Ga).apply(null,arguments)},pa=e.stackSave=function(){return(pa=e.stackSave=e.asm.Ha).apply(null,arguments)},ra=e.stackRestore=function(){return(ra=e.stackRestore=e.asm.Ia).apply(null,arguments)},x=e.stackAlloc=function(){return(x=e.stackAlloc=e.asm.Ja).apply(null, +arguments)},Hb=e._memalign=function(){return(Hb=e._memalign=e.asm.Ka).apply(null,arguments)};e.cwrap=function(a,b,c,d){c=c||[];var f=c.every(function(g){return"number"===g});return"string"!==b&&f&&!d?Qa(a):function(){return Ra(a,b,c,arguments)}};e.UTF8ToString=A;e.stackSave=pa;e.stackRestore=ra;e.stackAlloc=x;var bd;eb=function cd(){bd||dd();bd||(eb=cd)}; +function dd(){function a(){if(!bd&&(bd=!0,e.calledRun=!0,!Pa)){e.noFSInit||Pc||(Pc=!0,Oc(),e.stdin=e.stdin,e.stdout=e.stdout,e.stderr=e.stderr,e.stdin?Qc("stdin",e.stdin):ec("/dev/tty","/dev/stdin"),e.stdout?Qc("stdout",null,e.stdout):ec("/dev/tty","/dev/stdout"),e.stderr?Qc("stderr",null,e.stderr):ec("/dev/tty1","/dev/stderr"),ia("/dev/stdin",0),ia("/dev/stdout",1),ia("/dev/stderr",1));Pb=!1;jb($a);if(e.onRuntimeInitialized)e.onRuntimeInitialized();if(e.postRun)for("function"==typeof e.postRun&& +(e.postRun=[e.postRun]);e.postRun.length;){var b=e.postRun.shift();ab.unshift(b)}jb(ab)}}if(!(0nLU@i_emz<0EZ;%>}_lhHK|c!K(LxQJwq{%mIee*07v! zFkyS2H-QmlOyzXDUGy?Py}j&dm%gm^jESlPdngzPT=u#~tmd0}^QLBYx&g-OPw2hT zKX-h0bg!x!p-r6?({v1?dcYu0Q*X3!n*VzAplDOH$#wbrCobXdGeLq)(X-qV{{E@+ z{QWa4`G0nQg8lt-`#WoYH{0J6?eDW)PW|UtJAYv%C)wXE_IIm0n5QS({We!{|Ce@u ziuLte*P-N8>+7$q{I9K@+WyYi)8Do7Bkb>yR&ta*J=)$n#{NFmN{+QQALm^BiQ<|f zPm_KeyJY{oTGH_cq~80;`3|#oX&SqJCt3R7RjE&6rzf4b6Z^#ZIF1L-b%#;MQEyF@ zcGJW;Ds>Z|_)hHHfl<^sAQ?p7AL!Hlc(NZaCUGZ99vDqb#NCM~@=2%bsGlT#*Iku% z<4&6AYgb1uio4#&Q}O18^dpJ<0lRl8@ z01ig$m&Q??__Uk5&WdjA4@%>5Mf9K!Z+7yyNa@cn?*f-@!mOs^)TdFJPNe%qjERBg zc^)}>g#Rpc(fY(s}g=2o`H;S+Y<`IREo~UvRoG1MKR&*F=XEN@_aoOz_6Hyv<4@@7>e?WQ=>(=Q^cn0QO%%ZSp z)B?SdL;X@;#*Dd}<{*)lJxSwud7P)dOQ&(1bmNKc1Sr?_nQ$*=5j#<*T1Bsb-h)KG z3+lNfjXWspI&ri=BV?$3rkv&!cYRX0N9JA-?MLa#^sxMJR&3ca=9VTMtwxgiqF^z7 z^nm3P5%A(a{s$H%E(N$+cgG~bEkMIE>z}HqqHaR{ zSilqd>e@b0tv&x!jquNI^xpVC(QWt#g7O1I(jWdYZ2Lb5$y|KVNjsWO#N?7Br!h5^ zWuWfLNv(CCduxe_FY(FBNe|xWLGuWdgutgf(**N<{yW}3njc*kFIlq0Px~dTRlhFo#Z|icd{s& z^K)}PEs$&V&NHv_z3gp%L-cr%_6hF8N%TbbXVH_~xkYrGdt~%`?r);scmJ71Pj)}? z(No;t7SU7PA4gAfe-}l6;QlFzp6>o{^oQ&w8?Gtf3nHF zHSy2fV@7 zM*hT2?$+Etdz1Ta?w_;CU6lI2*yO&G`;#`gOA^0jllyk=w{CK`dBCik`6|J5dUQ||wIllxNc>rL+Z$j@wYKaKqFZUPaH z*yKJ~_#-#DALst4P43@wfAl7Id+v|fDo8^xE38$17)KR^Fq=PUV*`4{sqbBis{E|{%>0aeF26f_W&X?Tu54HK zi|psw&$2tSujW6_UebAf^3&{&>?hev{l8^D%x>fQ{p@?$t=V_8?_~dxeJ%NRc1!lH zY}-xQ=C5S$kN!QoJ^NAigY28xH?o_vuV-J&zM6e8yCi#a_QvcD**|9AO23(YBfUAj zKK*QZUHXmuz3~r`Gxr_^7HZ+fe^XJij1+Q~a|0rTO3I=jSiU zU!4D4{^jgT*^Sv3vd?EXWS`5f&pw-7mwhJtboQz2+U%3rC$f)c*JK~dKAK&fU6p+# zyE6N5wln)s_QC7}*%jIQvwzLrmtCIyOZMLEpR@O5@6O(py)(Nkdq?*6?9yyU_D|W{ zvbSb$$=;N`K6_nuarWBmHQB4Pi?V;nUX^_}xiI@q^4;W>{C!3C^6Y}_W!X!!zt7Ik zUXr~ydvE-A*?HNEvTrB1B;QJ2n7tr7H+z0|PWHFi+1Xjynb{fHTy}T*%k-{vSNe&Xw(Po|$pKb~G2Uz2_;{b+i1 z`sw7V^dsq&>4(#u=|_`qBp*sYn0_F=0w}#d{nzyC>DSV)rk{y#O23kRIsH<4WBSGP z3+b1W&!;z}pG!ZJzA64<@|pP4>8F~zYt!3`?-k!IzEga?_*(JR;-=y&#g~g4i!T;m zC_Z1@P<*bqzW8i$UGbUX)5WKXYl}}7Qk6bdyea-@admN3@sZ-n;={$x;zPv;iYtou z7ynwkueiMUm*Ty}ONtj4e^;DW{B!Z1;@!o&igy;574InCUR+x2DE_Hbkju!A}y$Ila z6+af(Xl3L2v@OG=-svcyJgGO%4m(UBv;m|Up<0{vKr0D%xd3KmeL>LfN`chwCO7!W za5uHVPldZx8~mzpS8ecBxLdu!uMT(98~k**TeHEh33uzXw$|SI4Ss#NJ9L9TG~7LE zgMSn=+ql_gwsG?i^kzMJJsu}dnur#RKB|)dfdmLtiDoWTitFUDV_?(o&BNqzV?~P~ z$!OL(@Ivt8&L8G{xQh>SF{4?~OFNpFKK8yIReZFK+Vwt3|qpxn^63JVY1fUuTHk;ju*?*wDx+XH}C+Cr>p9WRj6X|SGzn#m%v{cVas1B z3H&u~_-m~3&+}J)z+b~SjK3Ort#yCEVgUF^weO7i5i|7P2+Kwhti+3}LX$05wcDLQ zh+zm&EFmk!W0;E`p zrK{f)xN77C9b2Z=hH9$;5lfncdGSs&gNPZJWT2>FhCrVmK`z*BI|IPR48RZ`x9rg> zv@@UrGl<4!Fvc_oHu`o3@m^*S&Ch`G3y;p0n4@KJcp)2$4i?U?cD;WUsezMoe)BM1 z?do0EGcXqxM)hrdcQL{lcL+<$@9SyjcHPkKtz5Y)LB-otbZgWiW7E(8PX0NySOuBE z?QJby2n2UV4T5rihoK9YcNgqKC~EBm9JTjYn`JR86f;_leY-@38$?N&2bA`7hPyCVq}u*z*-suw+M4kGo| z-T-7X9tBt0-1{+xAi4|Px4dMQmZQy$ybKq_jHF3i`|)xCD;_TZnd>Z6OvcOAbD?t9 zkv^Hw5Y8DzOSSGMq zbIt2Hx?CwR(q)I4aPgPack?gXB1G?d)@!5#RBPRFhq-QO1kIysfD^ja-iU@t55*Fo z%e$kaOlN^B{4zRf%dn$XV%-}t9VKP3fm(U;L`27ctPj~*w+27x4myU#m9Gd2E>NdV z!@_iH)$BJ7)X+XwjE2=L4j96mdbMM10ty*!@Ma}r#JUW23Aei=2&>oobNID-IdN#3 z{YkQRn6JcOm6Q)My#wn9j(no3od8(>D|&v_>Ag|x&5OOcndo^lzr2ezK&_e=t?m+F zxwADvpq{ASWug?lQFq3H!07topT!5qZb&mI6-=JD?u;KHKFrYlj-h+rAM(a!KLhI} zliw1mC&#f0;PCUR_^D=ASq-F)69AcUeOcSLlsRjZ!jVnXh)KkN8of10{RkCQ_hD=c z#D7!qyq_U#fT-*b)VP}8RypW&m&|uM5b^hJIKWSAk55O0!l-(y9n-zpWGFMe_4+U) z%5JkVP_B7j$k(f{`vqiC)(N-eZ28Y|uf7lL8jCi}kezjryv2py)Xr2dLgT8Uc6KN3XtvJN{$75^n-6koC<3>Rar|Rt=il(8|tV z#I>t$a^;#q5pPs&NMagftgriFJS*86F*A6g(QLdWaK_7cHt2$*(HiGw>jVFy_S>pr zb_hRoLl~82TV)UcLIKWE36}=^uyQ1@8^Q>5>5CcV!iys(FzM?nXxJ23h7Pal&koXl z+N}Ao<>r zmBPg1h10+IM|+?C9MAA8ESP=`B>xKl=q>`lc{F{G0O;Nu0K8GHH2_e{BLEa*09d+b z0LAeH3V4IgQ?Zw8JwWL(`uo{L&{#gKIJxKmWFV<`kq6KANM>Kwh2rR7=F{G?(G zUq!r_9j9dtO(Onur*~Ay;~-P5WAR7%Biinx`obGV-K!7t78y8lr&#t*Yw4sj@sJniGw1)7#6iZr|5oQL!DCZb!q~b#I16 z<#t#!au370=JWdqEh@Id((PziH+=qHK#R)luxLcXs-r-U?fSLKDX9kw__}5Iwu1GJpEkpnq&18~EupX- zPn2x8no<;Kt%D-Q0KgPl;Rfpg{^IHzcc~dT#@t=;NNsA=s-assG%j`PE^{p53)&U8 ziq@%Hpa;K_Kh#o-_3hNMS>LAWb@$n512rm5{bl!fO};*xF08^Xn$b-q&m*eF<^e$n z=m`UV3kK=Ncq6033{w@fibf&X%v69b23xdwm`Wk3X$VCmds4Dpugt$Ngf{gul!?-I z-;@soyc|JO*Uwc1693VoYJ_Ht|+Iq6iw9slWQ{TnIm5(7^H>cubiE_D$L|GBDp$&mIagO2gd5K z$7JXB6mJ3n<@RLR>a0Ar&SaKN`y;AoOYZ@^Kkm0s)g)NDk@Wa;lip~;@D-x(bxa}n?9de%#3ZsX@J*UK8#%gk%8lyih1lV%~QejSz1XlhIjW5@_# zl~yM~F7)VeE|)Kia^720{*y<_%5VlTt(?+at!Yp#TQEnc1|wIlW;*Z45b= z+@REK-Jq1=CukhG{0MGPtFXuoI%e4o_Q?%8=EwygYh6||FYs3`e2r*vV5 z<1q0fj82TJoakH3Xymtrfdw2%=KGKzcp!-o9gBN|ivm(It!#_~dF@BoHQ)0Z5c0x~ zR3AuM!34%v6Tp{xbavuFs;DI=rXp_L&%tt}oxt;I&)ZvH3#NJe@ zu$*tgBwk?>L4+_#1Ehci8e1DA(Zn??49+G&$ho zg)$f1FsYYAcKp)91fpT#oK~+-K(ThX?&`>5J*9b_7#S{`c|TpKS=$>}+Jrx?M1LGC z4+bYs-`&4BeTzJLS{pCF5-|VIXp7Kft?S~^`FK6MrNFZYv*DqFl5PvekCm3)nGuqyZ&d6}W9VYwUS3UZmU9A za4yNnY^HeHfG|2q0FshbhcJC^o?LsE7;(MUB3^jz1~4Q16SMU)^vC@xzd5))HHgEb z)s>_Up;&%tO2}G8+Tyx02e>~?$kUGeeKyMn!=4`*!!Q|S{raznb3TOtJM>UtDAB9& zzOsC<0HByzS4+@%LK<(cR_;u51ByhB61YK2Rw=-dR=Or^Z4twYVr&I?GM%_y7bN86 zB-M#bt|OQjDVRvZ;`lq=7BO`&g+WA;t9aHmQG|~s`uI?Y*Im!5aGBOv4v(6eM3ePQ zu6Wu396T1sUata$og=E}AmTc%>t>CAEQ=g#P4%h3F1TY+Ls2!1g?fN#LC8-9^Kyb8 zt(@RC$O+TS#=~WM3;2P8l1TH6_rE7BSdoL{9AT`rIXIpcob0YrxWewH1XsH&1t~$l zW8yP6lmYt%jMace)S;jRu)5AdZr?sQK7g=MbQ-I-Galm|&AJUIP@Uc~*{MH2sd&Zy zsE_;O(Q@T)vHH8-1``w+z4MiztLWu>UuR8Ry?_19b5ID&-x}f3%UP}ZRyR91fa|$< zyh=$DZ>uy}zu}^5e&kJu5UNo1;P~8VCc+V0@uRF6cKH~?W163)1$#jh;_5Sh86O_K zlxh~!Imy5yjG<{^8Z2G%_Q@#FT6b`#eAo&j4L2+(hx=u4Khuz=i3x-ut**CQNMo&g zh2nN%U-dG&uEiM@fYqQM{)CDgb(#&j75iPS)}8W{q|`bLE^r~O8iRV5Lp_`&gw(GiY+C%x)| z`t`%9SJ@BeZvNe8GT5oh0Q`)Ekp0`D#mSglF$DCd({W)20k-!Ay|r_bOkd0lMqfM2fAzB z17Z3I3tq$LdW=9IS~PSZg$WNXK|_oGz_7&SgQnR~tX8lIar5Fl&GMtAS#AT(8m9;} zOWC|`$B&>US%rJh?Xqz_DU)ww56+GSx}Cxu0JVTmf)-9u+jdUbAZN8V15={mz%3?Ae{fG1DH zx;#afkj~MaPv@W{q;qtebdGMt)A@7`euQ)msdv~leWO~x5!sudQmeNU2MzkdvfN;4 z9T^`2D-lY!lDM^WQ6*_vxBI%>0<&|y!E#<&!hd)ZLh2S+c&8uqDOxU!yo3B3%TZFj zQJvbKGF$?)7@#-~JdZln64Zn`R`6y?y^F9R4-*ig50d+BM0ERG6{SE4rU;8*!LMZW zE35i#H9yNg7-2{G1Io`)W)zx7uj;{5MorIGx4N*~4(2R2V$bT*BXI^0m0}G2$0PUC zdU|%SADq3vWw1Z(bWmvy7>Gnsn}?la&HBtMsCYB6zP>nDPs+R0aaC{1X#n)knVp8v zRC!_;ptu1I>VMU8c&DP&iUv-S)KKq=j;08BkuV47Op%+E+Qxzma+_G&YGsN_=%`vE z=dV_my)~$dpd8**no+(5Af~i-N2AORNr;=S`V9(_lL+_f6WCoUTcl4dMqH^wNSqdk zdiA+1m4dW$m)la8rqil|RtpqDpBXH%=~VmIa>BRsS?gq`yvt1DsD89`*aR&*j7{AN zo6k1`xh9e!Y4)GDLn!56i#jx>Sa@$KEKrZKS^_5z_QcKljd;5;sxP_04I%?*lb;rV zJN~yF)X=X1$jfGzluKf^wd7gP`CfO)L`PvKG#f#Iz&Ht;RS-AzO+S}vST-yFjj%q2xZ|rPYLfFx8NOAGG6MoD}s@7 zNPK)#1*cD9Y} zn&8P!wbkNIrmW9B;LT%2mSF^+ettk45#VCP`Ks(GWors(;x@>NS@<86j;Eq&p#sN7 zq2_W%yfa?wZbL?yv1?+_1+mBFzb^J1lako$S&pS4cbjBsgb}PiP4*V^y)cL$j*{3w zgi#XPO%N?}MkJ?7FK;xsI#*QJulLE|K&Ftcbq@^)v0rsy`EX4>m5Xp4YR!R%#M8iH zz0cdKe;BH!vMv@>e|o5XoK^1+4ph5mL~ZRmL+v_2#wOCHtqU;M`Z%>Ir|Zs+wx?e) zs4xOW>qXN$15M9tuMsEZTHH5d)7xzrqdATSuyB%Z9gQ1eSTM`CG$X!PD6*a)Py@lw zj^4Ot(Hqw_Z(O4{9v=Djjo0}WHW!lPGYBiBmw}F~Xuw0aza8NN1-{6)NXbi?M)EAE z&f~M?j%?3iV>8OS0j)z~B-SJJS#?ZEqtNOOmrxb%a0yL?J6u9#xWkE*13M6XN=O^r zK)^Z-Lg)(*GK$g=L}Wj?{`T|YqhStF@VFekQbX|NWALC*1@Xs^cNy$*8!&~5gta&N0@7<)2&jdJ9jV`vrs@p%)Ym-!qRV(Z={GWI6IvkCm<^0Xpy1bR>s!uV8xz@E zV#5MkUz~&`Ow3#5NUmj;iTGDy-l`QB_nTDGIIv58S_>p!CR%vxVOq$3(l%# z;=;?|itZS*CP2QI%cPaA7AZL$(ku@dm_UH|PYdb*gl8a}fSoiB6N=`;O8jW|1aKQ1 zCe&?%VU*!8u`N@Ph&VV*timFP$#`Ii4NLQZrDQK2gz>;qH38cJs~`rkEMwRcVAX@A z-A9*}(@pdTA{Wi0+agak;1GAD)pW{-iIZwC+?W?H$e7clmhFt?pzid3!9fIaP!kmOev^rd8hhtr#fQ;k6{jTe)3(N`|zu}p)fVm%dJ(qG^u^)j+XeGxsXf3CjRsTKMv8*i$YF(ij7O*I_Z!8@wx ziD;npLDtVfp`U}wV?}aw^ek1yVmF^!GDvwhnNP9?e*-T`%4e!Jc`{PH#2Hu)4IKKk z=^!nG+gw4jxl?PXc^geX;MkQRkke?7yYO(3rhYiSKvB0v2a?{GJ5es+mz#&V0e^di zyH73c#6KuTZ(q(TGCDh2(27($Ym&m_kYYs4as+U9ot$GN@rG>AJ7`^{RS3P+_nn zzjMW3x^kF;i-Og|A4h{})kB`D)@A}^@Hp*|OzOKo^z#pG-$LtiJEJXCcPm>Ez#+Pk z;Y=MRol;^_i@{Zy1fAYg#)Ia8*0PoUEb6XiEP&pr02FfsR+oTAlUnJqvcIH=0==Bk z@fkAGekCAR&4Y1Ojn``t2>mz+**n0DM<5gn+B&PyAF_oX*=&947K;ngy@t+Qy>d>x zp%f5hxy*Hd%mR<_IL(%aQvtmRznC85iV$t80M8W@BfK5e-%uE=o|3nlILj;JS)MDXj}InPV}GxPGFFkPj;d}p;w87gd=x70 z(Y>oVk_D0>;(%KPPrBGw3?Zwz4tp}=OF5+iE)~ItshSRC z%&3_s)f9njpCVDzX3dt>0T)w;ENoSoDy#-pThq#gqCsVUNjS-S$()(7Ja*P8xs!Hp z4~-J!$Y^UCFyNIo3+hX|A4n^+E^#5VUV%-jB=7akk3qV?v|~kdB^{_;{@AYoM4D~w z&{}QrnDTjHFD%LjKmr;VIZgD6s?$gj$}HaGiDL_nEEg{Ek+^8&)pRh*uv9GWCRW27 zq|(4B1pr@v_c0J`vXZrawK~!>*mf9)>NA-CY0Yt;l%55eqb(sCimQM6!S8>Jxhawf z;6torV6!*^c*_GS=vU-wy+JF&$|?0lvxKF>1tdM;mn;cdC+Sz-WG1D-_|;koD!|F0 z0@(kj@}fH9s$gd?s^9{s!0<_vut_sTei3$%y0d5o_jQU?6~RUBqco`>bC6COPeA~#p_sM&!L@0s_@_j4 zN?5{ni12)%-&jV*xhfZS_SS)3pw&C#p%0z{s+v?O!v>+W zu0$~1fX!OzX}^u%SlEvb?8u2x9oR?-D)@(x&{%&rBJ*Bi zF{U1(HjW!d{qZz^KTf@VFVwpr&0n${)VRCZnn%Z@oeD#yc?sAt+MhQh6Pwi@PftV~ zEZDCn5UNEOfeaYA*H{!AmUin!%|^bebLlXBp0@HeJM}W#e1QWZeO?v)0Vkd4Enphg zmxs-0ybMDT^hVe-Ph8m%_#+1i@OA8&IYEb4t2TqQ1wBKX(!~q|0uR(gAvru6%4|YkL(Wq}{2{5& zJ`;0fNZd~D6no0>@MEHYfw9f=t@b!6S$asp90bx9r?4EwAcU)~{y09YPI<6lI}NEO zM2$o-!_R)k`q>F0De2uEksHihb%KOc1cQvx{Yhp!hNB~5%~E@~O~}P{I6A@<6Kw&H zo1Dm4EC#x1kB(q+sA(q8HPJRJl3UA)j7M3KdT|udXpWBHX>)W0+)O@AF7jy_Gci|d z>(itp_%y*S^J!9P+^5M8e40W2grEx}Cd$Tieeat|lnDTp*|h+MnX=~Le-#J!DEMW< z3kHZSjzRXyPrdn!zgC`IR-gZ-o4%rinEIvrC6DGP^-cmocGkNb!ghFum(=6U7$_WV%i_dQY{-~jiM^=sO2hz zBB0u7$7A6KjR*13jK`10!wQba3)CVq7t{Y?JxnjK5D|uzlS3BZSf}@N=TA3YE`C0T z`C>|db2oqFPcd0%WEa^#@V0&OyMJcJ>_QE}UAA;VLRjz7UWd{KRu$1zlVxcTm5wR! zAxCh~m)~N4xpEL;qlyt5?*9pPZ&(?e`{uM`wnd0p`4EKi=14N3zqLe6FfO3lVHZ~nM({tLwEwFJ)vUFwi+4oq zDwEjuu(h5}rC^!fWXb{&xTDym8`Ym86opa)6(2p3DA*p=Z;m!E>A9H0K6TO|xvcO~ z%sgpmksS0Dt>Aj?ifJOr%siB|HB0ZviBxgk<8UBhq)+>cV;SWjL@HMsQxsP=RXx?K z0HE0pOp3!#zWVQfbs`lRaWtSSFjYV{D$@B}G7fAW1U42*RPJ* zUB)J)yJGX}0ZNQ~=nOmr#W39Ux@nz#>e~Dlgos+m1{F z44&tX4U+slG`J%HO#whE!b+2y`_LTVn*@nu#0t_!O|TDI@(4jrKH-rQ7UY1z0~>}T z6I+?z)ZkbMdpR6*Mt;#m$MPA1^J4UyX3QDZ765L-q6UEw8EhW_48R*W1S4(xGEkG8 zZu_T6a3bPqIT6~iCrks-a9bk&0Gufr(c3BuNhv5_`JTZOh^euDtU`!bSOp>$==38SFCJ6<_9^TW z&+lgqgU(RtAU5y>XQ<9RV{Y=5cUUZ?lCkzDq7BF~BvtNFy}wzoeWu>|=6IH+lKlT7 zYfxW&No?#@K@Un$1n7sd|{~Y=>u5h8(^|V{9BymtrN_(SNwS+!j5jlRDeB)mmCq&$jNs`$9!e#U*>L7&B|z zXb@_~)ZBm1HIud`jb2*0hdLO<%&K!t;N9)cIvZaGBCqbuR-Ku-=&a*RuszgusP({5 zYmRDFLPe^ji>jqm`>m$hc)e^cvEql)7-i$yhyna7;d4yy~B zW`(K*V59GFR@1A+2Siapq$-sG)L~8cC_sTFT0tp0lUgh~>hQ5J9B~Pqr-SQ2d~@DT zWhaib?lm{FTWz?+AbjusfgcXRt7c{#jUl)$Rp0^;eFKe~>#k?&$*H?spIKI@A}T99hacxX_Z8&a%4tRli0XS11H zF^Hk5y;|p?MgGazloqG(StPAO{;q9|mM|J78m<_s-o#YOS9WAC`i(PZ&z(QFb8Zfc zRH+kb{g^G6o;zcN04k(pQ7Dr_rZlPsnG_`1P%75{@n;a_52gid`4Z*?4H!~$#zO2xJp$gV(h3ffvvz!YN_{LnI* zlUoQng*$Xdn>I;R%pM3&odUekepuIh3l_}ODMzDbGNfk0Yt$E;P|8Mm$_XOcV zVKPf{S?j_j3t3eHugMncfkcY(lnIG!`#E~3v1>1Z8!}E^>@*s(j-+#qhKpI&4E$&* zSTa4(3&(m3v#5%bg-a8THd%ETKiJKU65_eqR@JIqzh-50hO8CAX=H}6s#IoXL?-no zE}w6)4UNs!cc25p4eGaAUNlV;O{IX0ph|42QeX%?GMs?sRQ2s7byDi;hnnp$?N<#U zGcMMW4(J3XEc+-Lm-IdqGlM<-51J6zg*ybVJfr(7Sv`taJ@goE!M<)aAxr5Mi z+dXJ`<|N#?YGUOy{5xEL!=G57b6A&KM@3If(hv7g;B40pnb?D zEP|rN!)C{^6}H=qDO|YCczn0pK5a7|4*^^fW0uNAiMct9IeMA1`rw`MX*1o^q+vd7 zr4HRS6&j&3g=hjn*1Cf*)BQ#}RRg?x2|K&tKCT$b|a}4m7IP{k(oxIni430og7A_G>Im# zAcH50%YBtyr@;UNkrk6MXW6`lJD!u{_r;aFAScIWJ7ub?b{5XuLLxeW1E8=1A3H(* z-c?WXlfi#*K8Ri*zZpahCo3cqi4EBG?5H8eF>8HK8 z-6n_2rz>Soc?j}#IH^0Ws{^{aO4y9Z5N?6eZ4dQ?c_F~@*`8%R)K@YPCV+y&NoQZmu>4>(~JHE#z=k_fu$et=wv|iKznP= zP`>Fm0zSDl7_TI+!X3O>(#p;3Gv0_k*X~uMM&P~m+1D$Z=ipkQwbis`m9{F|$8H^3x>7Sas2ZftJJ= z0hYI31v%*KO;=xx(*_^7B)wJ|-XNslmN<^`E|?ph6y-YzVL$?@n;hB65$G_M0E0DJ zf6!)p#c`gER-8#8M}7I~PXGQkDm?dqDQ2A|jCfTAG8I*jBVmpd#)YU87mGS2v<`5< zB*IQ=2e!!C>!=f|ERH%!M2JrW_Q|UV!2bR8l6tVw+!ps&g%0ibbnDJ~ZMwUI354zf zHx=@Y^%vQ)n@!!PqtM^X9kM^@CV+BcrLa+>BkTF`I0V%Ri^KBv^g1i$%HQd)v|#`U z1Az?$D`Yh9YeoZE(LZS4<~?(b#?faxB+UQ?&DnEI-zb3v+dkS{`)7mf(X4VCjq0>R zP##bZa)YQ*3?^vSr!Z1o?bugT%T%gnU7ScpaJ_Vk&{r;mMGul~c0d8GK^;}clG4?d z+^UsglmwJyN7{VH0sjdc8NU+naj^O<)T8i?0#AVR6I$pn>mdjm@{In9ERd%G3~`u1 z)9O=1f&$JhIY&GY6^r>P>OfOq*I%(VfDmJgT_T}IF#qD(GI{z3vTzVo$YvC}q8N;p z?USQvSpJDdH-elEMEgscbO9iCa$|=y| zF5sE+wMoB|mpNqf;T2^iQmq7Q0EvNEQVfOJ+sn6f`Ue;yA{}&xa%J=h;XZvR|4?8$ zBIJh>8~TVFI95+%R7Mw$++h4@B=RF&Y0u{cCkE-5%b6bYZCk;@Z+!m)Z%CEp71iJT z+B;vC%5aS8A7A;pi?|igDZMC_7Ya2ps(*0h%U_l?)%B(_$Y|ti?|irFK<5LvlXdhF zI8`USbemoZHSf6nzG=JQxHn0WeZQg317j8m4- zf(`++wcAEk3@S#`2u3!V?}4X%9&j=Y<7xZC z7E{dF#3v6n^4T;6obr?PD)Xu{njv$M-Ns1C5Gf_2zEem?C4fUShf}m%m-ZqKMQjp^da~v2pws|EwIpejiNrZfXS*W zDyX{UBOvdGL`0Wublqxcc~%qqBmTLx{0xgHyBSiqJ;x%85Tg@hOsZFDUcY#=O9Tk+F_ zYCVTuqo1dS%SqvKLbx0sF2{{&B;&q8ATcrBffY`9!#MhR#}vIT*`avLx3NF0eW!p0$JOxVSkS-0|7?)9gf zlqjZ|k_oty{quT7fxwf5q$duS8lS0_0Cf+ZqIC#f6`S}?>!8jF{4nJ&uyI}Y z&~n4b6Oic%HfZ7`072mZ^==~46aq4(Ef~#XRzRw&z}3W!@voKFD#iwj2s(%uf0Y{T zLZc=-k$EOa!ze)WC*x#8sl2U0sOjHm$sV2%3`pbZ?tP^xr z=%w9wbm9qxBb4?&A$lv67^3xh2R9&r>yw)8>f8QZ zBR!bpVw;!#tQmHHJNhk#jQ%k@pj?&?=e_{H?pn7ROl-txMKD07749#{c#u;8st<<} z*gmFACp_STP%&pa=mhU3Z7<_{2WSN$6F^*sHNc#xJEvgE$Vo^TL&ee7#ZN-H-%EMC zd$|mN5FuovY;!`Rl7r%n*2C@{+Y8KGCJ$tb(1Pb=QpB2p>Le}$Le+gpv<8&#Y(@y+ zNdzs;E(>2f(o#Xr^LX(pS{$R(dnTiXPKi#a-mA29Vyb`}YL`)GpMh%4ttAk^xDm^a zez8Muc=&@@JN)4@Vns0mR{MYswb>azwt*6XA0hC#IxpG+pT`h5Sd(d>chq3fdm7aF z(5u|A=WE%mVI!rHeN@|^8cww@_R-AMc#BESGiDN_4~KSf5Q>@br)4JeL%_wD38fnW z+Bh@W(`Y`2q7n$TQGea|G@Bg3K&NIz$y#H!8ujO-*$`Qlk!?%7_wfkyyNyRDD~Pz| z9b0AQEM$%hheoYbd26dIgvE2kGT}=KhlhYc+9(y?lnS&#R5vsyQz{r!8q^9xRE=Ii zCu4dAUon$c*yP(0T!b%>3@Qu9L?GVa58~`%KCCLeQLNsS zrgWTf5OG^U!CYlXZfPfy?#QvHa6vnH78mWL=kUZ8Rq>GMSwXbQZa51sfHf&+X!}28 zCjAi+30|Gap`?b=B1Fp4zTu1SX}5AQOD%!46>ZfCz7td!^Ea6S$QQ>_6hKCuo7;8N z55VdyNgZFt^%5iWDIL>l9y**5hhYwfQY+*RhC#`1@^sXvF)aWKCTIcRpT>>d#7E-9 zV|tRMNHbImKN8Y*h>d9h)H$ItW1f(#329mv(T1(1FfG#pXh2#3ziclI!WTWKjs{uG zR5VY0xPpt3a$G+gtAGT#hLYJN)6fnzPT+@hj!|z+P`w%Lbn1tTbZAnuXx0d37Iaa% z(8S4WnOA>C6E~a~pLjn^TpVhA;^+zU6Ynn^N;7d{x@6L|6Yq^ryir{3taz}%m^iC6 zZ~+RUfL4F7xf-7s(;pC85{dKAg8m>4s>w6j6R!Y(@U9StU@?t%iRUZPNUS!Vl2ReI zZNE5CLWR@eQiV$yE^y=e)#$@qCGl5P*blO|O^=YL->6oq6MBVGJ(}NO<`l&8)PU6| z4s8z2YL7d%Ojfa*iyOTcc2iztA1Z>6iQ+_|a1;Iu zRGQoOmRNV0CT5t35ra0nb4--lXH{|}1+Lkf=b9LqQI&72CbkYHM5kE3Nu%#%AU4=E zYQB7&EF-D6LkJvs0J0*@G$tY1L`Z`G_G|PUqFiH@vEnpj8G;0~-C%-7JLMNWrzWaD zaam4Rv&2|_!;|x2KddLd0T(GK-G`%51M5$Sr4jKZvGN=*XF>pu*q3ArTN*9M4-rhw zhDf!HZbkg_kieFPgoZvq=h!l3DcD70u}_F9-;XZSFa)`o+7eA{K^3Hzq?(l~S)^}OR2YydTTgOZAPP_$8(^+K(Y!DDxJ{De)P zKy664?}udBv~lAHgu`+IU>Vy=ST2qf348Y5v{Ml?Hi93FS}Rnm9p2Fqmc|%>D9E|+ zOCbiikFIyp^N?1*4LzM+ZUQv{2l~7={h)K9N0rxcujI^>{}{hG2jU6rTI=@13z$7l z_h^;T>|t&{yOVy&R4#~2qmi=Ic_J@^%Yp0itRiVv3Wt~waV?3vh(H(0n%PaP;+iVe z=YW`=_>@4zUJIKcsq6(+ObYIcFL!37T5CEoHO-%e zrzvH`ro4}tFF=b?OXA-{uptiW@?x{r6b$wnOU_b73P#(*qB+3#MoPuL%|R&1DkG=S zp1F(*Xnvkxc1{(sa*b^aMA1hqX<^Z<^|Krj-5EuHEHkMoFguU}MSsu&-O(}>!5m{+ zi=kEDD6Rxb?Gw0!F{JPWwQLkAxY3M^c22mOZ8{+gfDi0QnBLl>P8P~UVMDR$4-4mH zAX#9BvW_Dw6n#V1<(P5Tv>rq`2qn!8v9rKiWXlLRSw_?op*M>u{84a%?{-+qkq?|X z7FH^(hMZtMfu0o**`n`aX71eR5L4j)0fptbO8UKY_!bWtefP2 zYoIk*2~V1f2AoyX!`W((k?hk~H+auIIfa!B=FAdY*a$nx= z-fw+ja*P|CRVJZ1?`(pb(|0lew6vjZ2oQ_x!Aa}3Q*n~EA~CIdyQ62g+wn;&tT)&n zNN=bQWi9jikfT=e_mTE@J&^B%GJzK=@XUsr%^agF|Io)oc>>&NJz+f7X+P$y$h3w7X^u&` zMPf^?b}cmotA!t}G7@P_Ioi|uEMDc-cWV8K-<$Ne$j6CfJwPgA#DT3sGuB-rHJPfg z47oSze<7CrYQ13|%SNq$Wr%O7MUBsOPp!1_*R&!N!(&QFz%n2Nv7X!b=iu0ljxO?6 zljAx%@*DD2{j={)eGG73syUq%!?4qvq-o`bn|-S+M--mkWgX5*F?MemX_WCTht{d( zIjaDjU!ewu3!qnpXG(vBxtg2EW7~z~YQdgt$?&qbud_%;!a`EgGu4kJ3dDs>r^-tg z4Q;Mo^oku7PhZXHAoposYxdIDx?jV*9<`Ugjxj5WxeNa2J8K%BGoS-o@rYl;@;^=r zOno@n4#D6*yosYD!TDwlAV&8K;H&mB^fcaB#RE?ziO^uVfx7zIj`A;5zScd6FQjO_ zsIt+v6G~u>mXoy?$9GvzGs&3cF9Jp$6%M8WdP zFH5m}SnKs9w94xutfX967*NmJBI0AG0kPGn_PuWjV{v%Qq3y)Fow-@rK|E?#o{{j~W`R(kFyUth2l1Pn72_$(nIz^p?yiV%8)?#t zEjrz)?J6A4d`EWj&0pYeho#u!Y(C84ES0YhZr!^n)$vtPNZ=fTOzQ`Rd+4oN zXy|yO)|m%Y`D|TQj~;}a0em&{6?wj9^gBFXH~LLOJb%>a_j>ULN&-WuMN47|?Xjhz z^C@B@u#9q;Ss$4OV_c^Bjg)zYaTswprNGjnZKnb+n}dnc@q>v{Z7Bu)+O@IUur`#p zYs2GfNHx;hScP^|0u>gmO**#YbDxkXo!{|^-2$bMWvEGGs+yQuH2BW=;5!;TMrb>D zyKM&#n+b>&2H(-(twK9^Dl8g2AAk-=mpEB_V+U-;dpVb-Gd_404gq6$1NyEu>p139 zdR3>%1E9hY^A)OLhb1&$?P{Z0VbZ9m!v80Dz$7c7#1ZD)Y+xcmzx%o-^4v957 zsjiD%WD+n?F_V?wIyP3r=DepUAJnM4uU(Gt>_@fdLc>UvXB@ULKAL@wI$w=Es&D{? z<4k^~zk~xB*iGZ!XFr!~=kF1&`6t<0o)z^z8F8|hrVv`t2jeuIzRgQ}7AXs8NMhY^N(tPiq{c}_Rivu#_4Ds0H@8V_SrZC3oY3vHo|^Wjf`fN znnu%Xb0oYV2-tzq)-aEy7fZ~V7Hg>3`$*<{qmwPV?OO5?K}jx>KFH;`_fW+t7z!KU(_f#L2m33ksz_~oMC+8W{^p&5bN92 zxF)mDIOXdeAWZ0h3wODbhQ#(m9nb}(0)!fP8l|0ZG$VNivx_Q%q#Ctah)-yYe zTjV*yufn)xIB}jqDpQ^C0vSiM`ob%8OT%E1CMBZPM+(&)!UkojBivVglF~QV8PlDw z4JuiEyF6Yxfll4E%Z?~q*{f>5&jU&I_=@Iw8#W<|qQ=9b(L;j!dFX3@bRrlVzd^-w zg04ljIN*Rsshfvv{M$zeDwhb2^HNV^4SWUAbF#5pIw`cj>$+-n%_rC{f`1UlP?5r90`?h`B z)w#CpgFbxevDdp*P!SKDZ{Oz3K1g*vIf$SqyRAkhpea|-We~!w_`HVBk5)tHb|(PP zv_={_WhhsThR%;5gsp-JVcVp4-|aYrmbB|be5CCCXnC}RnZjieGF%d6O_flGK{%Ca zdFYT>1SfBWL?bfeEdd4!n4J<=z`zp+N_YMeyU{nYsPmP-U?r|TL`_dK ze=Zfsd#xQQ;_63+0xO>miAFsNa+uBR18!ac}7{B4*?lA_-yM`xvs9vKfL! zayv4QF)!dc)j3!uY%~*eytCAEj{V16zzXbxSq~<6_ z@Wbf=+!~P~&i*x37LJ0Eya1A%LAxI{IJv`uE+(QbkcQNO>hzxSBCKwBU8t>}2Jld;xLeU><_;r=>nR^Wi} zxs11sr+EZ>F`tp=cTp~vA|KHD!nSv7@{w94SPx!4^GNThMZ;c6X=yl}u&ASyv_~># z;|F-nv`|8zUk^}LA*S{){-PZ}E2mVk-668)@i&0c__d~nWrkFIIplFnM5b$SPwI~K z*Zk$4wDT{lCfZ7<2}PPCQ;4RTz~15%4X%RiirGX!81SrN9$co0G`aGZiQvx9)uFVa zR8(QciOtreeS-ib!ZN6U0HPPV8=Ed!q*C(2biHWM@+Ks=eMp40z7WEYhWeZ=Z?pqe zxLqH*U)OXmu7d~+a{NR@>^u^VkHJvp-i`qfLeGrBkVRu?D8Df{(0`1~;WP8S{?h4% zG1T8fucpHUwMnXrRzb@AFJZorRRHFQb)EoB09U;nE%ES3D9|jH{0XdLNDUt!0w`a_ zzhVTzt#4Y=*a)zW+5rS|4qv>07ltX3D*_}nqhSO;zt$c7t9yr6;KEZ(BP_d=MaXlH zU7Mn#cBP3M9L)S`kV@efQgg%t45z@>d8V=`&A=FX*fX+MexZ@L2*6vmru_=~rQ%~@ z6ed(aFm{^2!5JP)7b0#_@P=rL#^a+U9;$&MThu@`qXArd#P88X*Jv22ciw zv|EN9>m4yDWA`}rm5K`Euh8L?_aP&Am0G}ZRT_}GvkpLd!C{T93wj(p0y;^CmTA&& zSe+|>7g?fQlVSVm4U$!R0}|AjNI*8AJMACOw@#R&i)n?Pv_q&`#ttu9MGMik6fU+P zUc2Kneekhi^CoU>MW~`Em8hHrlWNDOAjOpPYN@zAvQ{Xc(}H0f13-VZp_qqftIH^@wEB^g4zO(1&DRXDljw9k>Q2j*|1T4 zu`{5UUB*xhG#kk+wLQY$g=hTgQ&T91qisoLW6SO-7Z>U-pHMYNeV7IW?-hRA>^r?{ zCw##Nh)-vPM)Rtfa3mYkQTW+8nX0-N=Kc4j44tUk!4gFFz`;eLU^qzP$zC_ze|7?el)7 zs!y1(J4rZwbxgAy>rXaDqJbP*XXc$qu)6DAaX1xPD~}{%CgjEkaBkEFvS0`UUV$~7 z+GqAG!`{_!=GL}U`Mz1iiqxc7#K2|X`OkJQ(L=THMODTiW@hkN%`F(2GBwY;e*A_%#Y=Jb{N{EM+x){PR=(I zce2|NHfErsuc@#`WoXQE(4B?Uh>p>Eid)*Ut_g+5;hQ@nHS{uRd1!e9V#3zl&Vfv7 zK6p{qqng0p_3?-=hArC)WqsaOA((O9yt*tS!iI1wm4NR z4^AC)OVt#YiFJojyw+7(tXMImK~v06_^qvtecZw7lH|h@%Yaef z`HGE;OSKypS3K|H(tuZ0IbO7o*xc+(Nu(KyVKL8j@S*tN_!wpO$cHlY^~akJt4Sa6 z^)(WFvuhnuNcqvukB=RIuo6n*W9v(h_ZL+&Um*Xjj)u_rjT$0HJ&fvizOl#mC$if_l zTY1X)ajGUtNNhn^|Kl|vj${D$%j0`=VQXXjX{re;m=7W9J<0sxu=LT1pW}zU?^A4U z>}dO5@1iW~&mR#OFb?z2H=4D?>R#cEmH&VHnhxF&=6<>FKxx}F z{Tp~~4a0Kv`QfuZNH+Ui7z4QhE&;T`aL(Cc&~!5H|D%1z!6$@f`G63rB|Y@Tnq^ax z;8*@A(nfhK4&bMaD5REF~Ec4`Wj~H#@5A}nO_%% z9M;7(>(WlJVH-^VAr>cTD2_A4MRKE0aQ%*m9bfRt@J^aTEQzJ@r`<&xF+5*BLHZbM zIH0_0n(ljtZ+xn_n3Zkf1tK1{u1{`TkEvSWLk`Tao|A_jO^*Lj;VRTeyp<1$Uh6sR zA?lGt0p??bRXPfn3QdF?C#*_B2W_iO3yKff?M`Wyj2MjBi7*Haw4<>aZ8Ve`ms^c_ zHaDi{4)*Ery|G*g>kCTZBs@bV^STQpMj?2e@4HVZ^sb8x>VPQBk8i)Wfahd=VJ=H$ zP-Diybxi>vL;p75w61Hb7(hnNHhxT9Am-$8vI+rWR$)>6_`f3;0M*~DVEnrD-VxE0 zg}0oBzhKeB9QSF#f7Noc8Y7wZ-&yQl?AV)=gO#u|T%bW0K>p22 ztEMA(wF*hjzF)|g(u*NsO0R^3DV{bT^hiR&6c=G5F=r)AF~T-sijt5pl{5)cFbxEZ zxXA`C`a!}}6Q;+u3Y)Uw@Ae6FiAk)QlVsX(#D@GIS^X}_Nwx`<4-%)7SrII9>S!a5 zTnu(hF`+xbgos!GUc#Fq<+x{|D5|rkD}=I4km4RN49B%%7|se%1ePeq43zlM<|T0( zaGbh^<9MGa1IN8^-0B7#w+i4mmgxvj#`BWy?Wf9^GOmy2lYoK>5>-d8YVvqu_+%i{ zXI6H87qL9JtIqENP`1`h4UL+eyy5&V*sxX{z-&MeDJ*CoROwxp+xN}H2LV-3c=RQT z_Mm&oA~h;ZwFX9f^?&S>X7?6)V)a4y`d>NjK(PPY;H?~1ePaRyGpp-=A?WARa7R;s zUOY*40intfAwVrAYNU0UN?Jjn`dDIBB`VWfFW^c$=)|fGg(??MCFDMb68p>(7WC-% z+x3d&>tde-EmK5kHXK7(M1S}72l?h%*MCpMU9?}+4VmoBQ^OVzuvVCc?o0e*(V&Yd zBk5-Qk~mJf@`2${KgL%u#9O6HZd*O@Wpt0^fIA|gY+`Q?2OoVj#WJ#yKuvNxoC`)c z%6ll}nIOuDeC5jyS6okDF{7d%uMW^Bb?K-dxwB-U;3RGOK>E%fK#oq1)603cvB zvJt#H;mb6819v+t1Au|s+XxYloVq_jh}jem$q}83a^HeLGW{MvfRCy9IFErq#uXrd zo^)u2oIgr4+XD#L%_%Hr<@DX3&7P{2Fv1>reJL0fr%NYWsM6H=f`jv<9aIEvW4 zK#B$;cE-IR(izw(A|uKpSk@r!ULX>XLZbsIq|RYEb8bPPMNxfJUwIBbit?>EHS06Z z(d77v8zorHEtMivv@}Mga6zL|*Y?fQj#25q{cVHUfz+BeLplUg=+^JL(v`Q{G3@PY zkjH2mxX+t~#XF40B~W!%U){ddJcXi0G(X~Fgn)$cB6?fh;=Ukyk~Q`~^tQfnv%WY_ zz<4Ek?1AVRi5v1~4;a6g=pA22z~aBRL=UZNeX+!_EF04`fwd^P)%cxLISbu5;p?xn zBlmMUWi=Esiw6GY9|FfY_?3qrS@_0h?F}p3?F}E^xN6}W#Hne<3U_-W4sTq&@Qsh# z8&-bPsTQo*}&>BJqua~4Ot^k^O*A3r!2j1!t zPizI{0-AxziZFNs;Q+16F*_z4#3n`>$@t&{4}z0WIhRJz&Uc;-?S&ZV_wi~;1Y-|qnI-U( zf+lZ-NSQ&_T!6Uz!u4yD@)kZhD8;q@>ThC}A6z-vm$)VaRjUbs6du@oO;@1<+9+R7 z780mPUoZphyW%1@0zj7=ffrjh0wuwXz;tZiRivl$ZUlZT`YveGdyQ%(wzOIh~;ID?B}^y-_i-c7-Uey2S@2W{Bx;m zp2iA*(h15wnybDZP$3+gjAK|KsB+hGU_)!_gzySvqP6t;$W^`z^ zn?JPL%^zCr<`1p5($S&SZvN0}9<+y6)5zGN)e$4rnGB;tt2xf0J+zun){G9V_8R!^ zhgR!w?E1r+Rol$?q1E%H^M_WCm&ssL&T1VH5R5}&R+sYeJ@fYxSlDRa5QdyE2#tnC%Wy@V1w0SM)xl*?6t1_I-fY z>_jnF4y7vZZa0%A_)d z@i4!QxXuLaQ(J4o*z3E$t}j3+B1%EjJGTgXO!=1B=uAW%A_^e-;gT~sA$4$XnYw}K zJ6Xq(?*2b`G~Y4dLvW+dhb+8tF3$5b>vR(PxA5Vw{}-L-oCZ>Xe&yfA!*JFKzE3%H zw~1wFld@Q_(6b+k!w*H+nmRzANDKlkoXy(QRW!6bxqQD?lb4(N;AMxSi0N{y$@fPx zw1K2PwWYeElhHXw#^Rd-qnxCETXZz|iE*V-fiEgKO0q!b8*#eP$tZK`fH8KM2aUHs z=n7(9Uq6DG>989#PqcUOV`57*rW2G<`x|!557=>M!TB`27!J81>&&cSf|X+j#%Q;* z7%@RX#p^kc3Q7zBP||>oLJz-0zc%kRkjTa2PEXNM!1T*n6OiTn0#)=7EVJF=U2V@Qz%U(=k7|r zjlk$R71y}TXsv09gaW-m_4CDWVis03Bu2)kLG7qfsD5s)O)pz8`WnvDj-IaeW%QN- zr!GS4aCEh9C%48r(+cZ^@ig$VWVMAo62`F6?#uZNOm7TqWaMaolQ2iQ=FtCw&s5U; z9!_rn=t_?^9|ckwLfNP4rd9O*AtqJNwwnY$x>+}^N3M|v^<1O{xr)Vv)3c>`ETE|{ zrLD(&PF!yXflW)+&a_PMGzE0EU^YK#V3vQ7nLC~dV_TZ@Dwy#dkrj~UH8m`715gm~ zaDAEHf;+B$r)^`<*54}Sdp~-G`IkD*j#KM^D(jBHYMueULTL5)aEc<_mkv0xsd9#N z{c!kSUx)u$2Blu_5OL*19;SyCR1T$KRDN^s10L;>d?!9IZz{6iNTXVGH591n zUVUXC3@LmOfia4iu^P5;Q(Q>%2V~*n2yys00^CQ3XvTaPK?)syK%sfS*1$`_tTh4# zVtVI0Dw9fz!tA)UB?u7R5Dja8?FdON{EQRnLhG z>R5r?3(@sH*yS+e=))=pq+l+V#)k)L@Sy|X{RCE?gjn*Lc)Qb9-F~>#VAShGD$h z6-qCp5DR`!Uz2v029VF)T<1axja^6~0;!(9x$)6L3^USQdzUNRz64Wj3to(Q4o5z< zW1bQ;8lA_?=Sp8TBosfMK0Qx)oxJ=^%FE7pCgn*W@QliLo?-d1XILJ0a6Kt2wgQ^- zvDf_F`o)d%UcpAR(M0PmQhJfk5o`kwWV^R99nXphKjv`~ znl;x(zX2ChK%~4|h`k%QeE7FuPV(-5={?*W;0DKy+#q6zFJ35Sr5CkxF8lcH&;F9n zR=D9pOobSmVk&sbICC1^Y%fpf8aWrBQJ_V(R-$u~ZXUtKBKm4NuD)a5DKmc6z^!om*zoYL>HqPJeoK@`{V7w3|xbSeUqK-L$MqN=U>S5mZF z%)#(!0dWBUe~9=hzrZ=WNy5%lUY@eJbupFy{q1pfCYCC%eaGH3wR%`C0lOAFfG1%; zcxMNfN$lllkzG|ZbH#wqmZRqm`no)iRFhSa#b^n151&@xNa3mK=1yt-?1)&7o@Fb* zDM4aj6l5snzMZ!#0MGtqBmfng%F{1>(Vj@!{H{lkLe@jrA5olph^;73ZJa`5x5z~o zLgho>PS(^Ptuqpw95aamFFkdfRam&)#pByDnVk z%QJTJF5jRJF^Ew8LNQLJQmI&r$$c;l)8_uoL!bBt{Ufk79bMug8!9plC9nL|ly$z= z=pvVNGDHkvj_VAit@22P8l>fYUyZBSRrMz-rt`ioGcqKnnSGq8ra_sm9X~VGo%u0w z@Dcn<7v)TZRg!Gk%sz(2oJybt%Z6hUOLQ9I{=24*fB_UW^;No#2K<3P(||L*&0pNj z>HEtROnQqj*3Bp7=sAllCAU2fvLxO>CeF$7elG#8jv!nGqz6|g?t<|!0rh8o?ZK1xGsT$=q`b{Uy@|X;7ru)_0LF$Mi=R;BUm=?&OA^Qb&TD@XxM+zuz z(2NA<7y(k>Ft~|_D!7T;Gh(=EpbFsQ-7vU`hbrWe0-FN(zL(&p;BJNOGxG51?2BTn zat+&z)}0B2DEnA!h#O4dhH`sXJce_z&3z;A zzcml|t!;$xh0Evy@JONM{6bX8up2=}Xg9>m1#K-g{EgK+U;bkW9`t3nx!}R$f#Re1 ziZU9+umRONtP{)X<({9Wm3`DOIwwU{CJ-`7S%8_;d}{Kwt*puD@oDeFsjn0$Qn9r+ zO03%3Er`$Wam>hcfCw$1j#=d)qr8CPQianQi8{sEty7$dSWh`bStu@5IGq)6in9Wn zC@vk^%>+3#vN5%ZHIGL2j_HMj<8S+pGz}#~u5kx3gnTHn5`RRwVUd+BO*90$_9d~U zp!CQ}<0=X&Y=y{5x{)CE1B~aSqaiZWTif=Q_+RUN`4KIm9XURcPcuY z(8{pq0e+99aVPAeV7r-yj(G>6xS=G3C>#amb$7fPv+a*Qvx!RahelX&`PaYy#M@FW zSxTq+7w>%ZtHi3tq{ff3>xnmUoI+L^5u1ErXV&NjZvvGeOJ(2s4`Qb7^kR7U5xtO6 zG2(MJ_~={S`H+S|Do}9f2I^7+&z8o71rBxnT?$CHP)#u{Ly>k~fgH#wdqHmiug>n0 z#4C3WqH<^Xz#I(cxv-sYzZ;gpPKEL}?lo^#Qhw?4Qkp39XLm6xv**etwDqj0_j-4$T!u${l zELsKB(|57-3A57XBsph#QsNTi{Tr14(@g*Hgldu!%S)L?1Hh&RTF|c=pp#o0K%X%? zeAtFj4KhMq1Y39Hom0L?kjh}cBd?7{)gi#pQIgbe+gpss!$}N+x$KXjT`CEs*}5xM z$mADx!eJ@^mw`j8$+@kMB<}BUZ3R;RbtLbeLW!<0<4hS3T?Ht3FYjml zu^q*69BCefwZ(`UwFaW)XrQQDP(w99E*j1S(D)D_OJETI8b3u1O@ORTi4IHv;POWM zqg_9LOBdSpMoYqgI>%;cND=I3cVMXjgD{hKdJm^W8)7Ji6{3!~xpvcvr0YEm?ECWg zL)#MG&+$z$1R8wJ7ha+nA`6)lc?*3L{i<&sH_^dFpaY7}VL7mf@J+^E%C%f*s%6fO z1Ex*XS)!}FrNkt$JbTsXifo|2PQiEaGWQZTsVgl{lK(fYSAiQ|CQsinZyiGIdP{opR(pt^+SA|e}Ibb z-QmhPhvTLojM>`xus^Ca_M);32pYowIA%rT^3d0q z&0!RE$v_X!#A5IURLgvM!nHI&`ya&|477PWr3T#%nnqvxN#hAZ!^M*G|ba~JVXVc;-3=8#j%qXD9!|2Dg4S>%ELpluAeKB!!l89T+l2CpH zXDM!)@`c=A!`E|rLnNu2B zA393(KsDumjH;{;AD3w~Rabv9*v&`V^xdRe#Ell382A&xQ7L7FUR z5-hUV2`#pL4Xbr}FvWE81FWW8O`5U;tbBBZ6+RQWE5_377!v=WX+jpfK+_zP9mAMc zmb2Zu9Yaa8oV_e|j3ryL0&d4x0qhvoxa(~}V?*qSsL9CTnKAO-IqaCRp&etvnXnER zPWBv0sR)h)3_5`ul`a>Q#BufIqf=bbb@3vggS|ssaG2~x&r0CNdm-VLW2keQHAusl z_yl>7sSvXgKU_k{i`L$Z*An9S26#ofrp;L-PJ_DSi;2q5T?qwaGG0zAOK2AtcvaW@3 zGUv>>dgPr@FvjK{!CB=67G;?_1&Ma6g=k3BSEgx1i_St>lSqRR%4lxw|4P+=9T*Fo zqS4L$e_A>v!Xai|PNPaARsSKdy8jJ2g@D09gHAmab3F+{GG;0Y$ZgUo^nqujZYLyT zZqO+Opi`J(O{dLd%ojwbjbzNyV8d8RC|A^nYNJF&=cGOun{*)r6SMzMnEmgP6LGuEWiC1STm@MvI67{>B~no&jpXk92Yn`(Q@U-mO-AK;75&CMhR#6 z5iU~Rc}?Djtkr^2mpyL2yLRs*sWx?rbFX%pJhE^xJ+2|^HK6nQXkmQa0+%8@UdT`! zk{mGSCQZ=PkE-Yq9u&HidgxMW=~Fq-Q9&7Ul~q+@_~HXmLn40E4-=-W@tNC(W_bl? z3A=20s2tJUx;Uk5Mf|*DfWt0Z9(;hUz$W5vI-*UKR^s$rN!lCP|3!H9E?Z$m>Mq2q zUduxBexp+L#nXQe2`8GRFZYXWgOuS?cF;j(V26Va%B!$@zD#fw zC}AyD4nclwmESpg_7006lY*i_P=V~B*n2=%a_^YI8|wt?5WvdUdBJqV3Mu8x*3_KG zMa<@MSp!HWPU2)!rD&L6ZW0Pmwr#r%MVa8h*TRA&?MPx@fFW5Af5S9OFc z%L?YDGfHOcs0BeHucxyh7Dp_IEAmB_+KR<(Er{pJf_QGect?JPEC_V0Vcs~F_G$}a ziUH8{&8LPsa+uDH9n+z*Ae6$#^2X`qW(z{^7>g=zoZX3~S~UH*nNGzNoEVwPqKsk> zD6K^qCOCu<)G}6_rWmCWlY`G7fBnEbpQ`K*c9&}U5>B9tLI!>p!j==^16)bTpzs3p z7F$2aeoV)?XfTQt?YOd2q$*sTyIZP*LQ&`8@Pu%%i>?|BF4V+929ZOP8ceej&&`|PWGaQbhcnpnK z3#JHM^6s({B;IhVWAtX8KE&f8t`aymI7Wp4o(S$RKB8%EF&;WXZiy%6+#;7Y2Sr?+ zyjrdV#hFCAbsb`sAiGVd6|Zk|+A;^OZ9%O@-B8;z)H1ig70Spv?y}r>3}|% zGbLEXG(0;cObOg6qXc^`#lgT$9o#8Y1z^F^au~=$JOEjypag*{N*_IF_?VhBSln#Iz8R! z^z^1qPgk9u-rDKus?)?aRGpq;;OMdtrY;Z6*LrRA<8+$--=gUZ(`l42+ZLzCG$Ke? zklIAI7d`eBrN$-`2g? zC{LZUq&^Sr31Og@gtzK~cBG4Z$2y#sFWfm%0KbHO^Bwt2K8uJ28QJ^s_M6G)1GV}X&NKH< z7*EQBxhADIdyN|F20bjr_J;DPh7vDXy6K;&Sk1AmW-m=TNQTCEFxct zu~c5_Hv3izT>eWw2iF(7wde;6YoI|Lii!{GIk*zhNNO`fj0K;Li~`A)R8j&;$^D6! zI<}IR`b`4n1)`#M`4P$>c7Pl+jU9>U%0*_b}AZ1H0$$y zVgPBGDpqZ9vEHlB`l?-9XrxX0E`M(1cj2)n#Soz6sk*Z70^kk`q0*G<%Bw* zZZp^zRVd#)?9fo5$P!7v%k)?qtNG>VW{{;?(OdnJvyxEZr@*hUNBOVx8(%yTqtps3 z10uAg&9)slKPinm4uD>M&$#f^!pFa%Of57-k7pmVYc}ztR4m*^N;S#0u`^<^UtEjw-HFL}kUgkDb zT9)VG25f>hv|#E{t4a_W`Xysggjf)eE0J4LJCv$3SMN zOJs8~Og~Y6Jypl%^{|KosYp*}?~V(X=I|h;W0qZ4kMy_8+~9myqV*M#ZNK4ac9Xa&WRT@!uyIDAE@aW#20p}) zlzltzis=DYj8|wP28jC+88CRDP{LxQFTaCV$u1myV#pRVgOAPefUU4L$jN;T5Foof7>5PY0@XrZgG+5n5Yiwc zR}*4@I|RbZ2eW3=Us`GjmPTiDa(b@kdG^1h#jIA!x*CXFQY3Ca)YkAr&n%*U&IO0MBG?;Nm(0KNLLBLiiOF*z`@$us-&VzG%Sd%8B z!E?Z9AZMb}tr02!-GgbLdM&0NL$2M&mROw0q)DbTr(~vwM+VU6T&w(y&ACYx;uEyG zA5%1!&Am-z0#G`X1QhIJbxO$F#aD16yY%y2p@m)M2x#=VO`TfHilAQnR7wlB%Ny8% z^~nV4YXZbX;%(KHmKEX+(SY$~l?Y*-(hb{^X)Mx9{JLvd*jOP?R$G;&UnaYZm zh@ktaJSlL^&tnto{)!~KCcVY)x=KuK@2v0URLrNAqk{#xfi-EVOEgcAGPcbPwrh}O zn4<;jqrvAD@;5|N#Jszn7k1urNST5f#@6z?kn&`E7p44QjZtBk{6AEBBsY= zHiB_jn9ZDku^hFL)Ra=K6xR;c2cAoZu8`rXzTBElo1;df%9|^&<;}IMmagJS5wZXl+gL8!lgV&~_*osqfiaqW)>NugPCBf?!bfJTdO2DK`H))mLS$|mhD1gMqR}}tHLf6H45CH+zWNP+ z4~nO0KyS9(G#$8Kg0Z+*c1o5f7hHgFd`Jn4;Y>kRJvGbkA-X}=yA|bt^XQbW>{s1j z~Z3CSitj6!9yP=Nm@# z)GeJ8TxLJp1~MsR0B^$88f8f-%rV9F%EHwg8%#(nvq;u#b;2gujCfHWGkAxZn^DiS zT_sF1N#tV2Rf3uFr&(2f6}?7(d55$-@Hi2U@C&OVuo^x&QtaRdO<_K@RS~MB4?ms# zy~fveQbH)G>5|dxH+^xWYC=G7My2&?qUIdJ%~n);rwo;}`OvO3CVI+YAad5sP zh^Yo_6Ye0or@@_>*=|_dP*J_W3AnTU&d5${sLa{i%+Ih34f6^Ng=q>&PYfbHQOX(E z!oPk(O&SN0el9)I_!HK?Hs_l<4acGPnpvz2l)rGV^w6VF#01QPv#7H-iMQb?P>D87 zuT@?S8BAx-M}KLRP;FxUx>62ivtM*f&!kfl(Mq$~m8QeB$~VD+cznLeHIA)9Yo@6y zcRRTcGe^KFP{^pwhN^jl9QuJqrzUz$z?$g&1i48+Vb9b}cK7%xZhZUi!ojVu3F`F`9+erU8EV@)FV6){|N379SvDW+merOxPzOUh)u*l7bI21Xzal?+&_(Zwv% zeS;a$BbHO_Vpz4+iEud7e%SCNG{CkpXWiIEF2Qn6e?MgW-;!S?(p2c+plzi|guh2hyZv@6f!UgSzB?!3@(A@>LrX@E$sO+RH`% zO{>f9kc-L<&>LpCU9Qy&4uMk8+ML~cfWUP&x_7mbPJJ!=V zKODr!!V>y|Bqz>1qY^Ppk`&_hYzZQET=<-(aWN`e<3;g=tEx~wd0jjwq^%7&lwEY@ zv7GD>a&3pbdHhVQQfp0-O!5p*i^FON+JG&5Gc2=;qt&s8!na-$Oe+pHJ*>M~b7O@0 zgqc=_*#-%)LxgH5oC#Qku3raA>5`G!kcM1H0eGM_1t231nUG6{@`0i{O;2MVXRW7w z2zoG;yVBEqbdIXX5xx_XuSW*jkfQt{5gFu1hucIDlyio1OG6>D4Zln5Vhg72B)v*w;D9&qNNlW980nddWrvL-yQixmbZH9>(ybM!V-v$tp* zWNApyLiZ*`L=BOCD3$+Q)VD5oE_i}8(t}kk#+9P%Za6G%R2&U?L~-NQhS8jTFHUqp z!NJN744c6c_b4CvCvouAaoBd2E_zD)zfEEQZv7}(!j>@izWZ~{YX%djJaWD>IaN{^;2BK z6qZ)W1q9Ey0VHw%rk{gRnz)}CO15fP2`sG}{@qGmS7V^rUbnsJfKB87i8q>1E_3OnvpO6{e&EJWe#l zoA4N>`69BeLRgd0fiR$Y8lqJz43-PbRvrM7gTQPu;%hbW41C;+jx^~ori9?;zZkCt z{3g4*J-C$F`!`VB9*fbB75w5Q0hN-t^{SF{Qc%qe03!qS;tKflU{K~>;?;0(jjhc{ zAF@4gC}gb6gm_mTd}W3;?`ma+lGVx#?;fSB;%cpcugqA1O)E3ac-ISBnQ6qkA`^-U znE9^qTQE)SPDl{1)p!^!+4|&IDHLcE+~uaCLnM`&Z{+3{Q!RK<)ii712_g_O0-2Ui z$)hZlaS3%!p)%SKyFCQqH7A8zEOng>s_G15thfb@gVouj8!4060T;yUK(^##PvVif z(ko-E^L$2^u6&x~Nat?({5849b$8hs`Jm|#bEqZVNXkWby?lr*889s0YLFuqV7B^5 zipcorM@%n$G~WD3Dg*mxFyEvj+-){+8!v*v5gAlE6*J9|p_-uE^;)Eiqn6RJWmn(& zP|spl3&vex8_zFQx5vfd1mDQ$jE{`Yq>ql-A7d14a4gFnP}Ei0;BfY*S80ReL(}vz zX=s>)_0TYLhAeQCi^&$1!(p9Ohnv6+J8Zk)wuP?tS6PQ)`+|;SvQ5%y!lwo3Ch43H zJ|i7dvsL8?H8!hMmsugXpe|#wp!El8B>SVO6Y}4U4Budtn47uw#1e7rYcD-UztLiPe*6+ast z$G}$3O=NZL##LBW61eTrVQc~{uF@p4^8svR)^?`S9v#MJmEScEtov5e76}bl>zGT) zP(o`s7}I9M8ct+*O+VZ8C2{1FY+R#RYYihL*_8I1(X*W*QdMc_NgO?iqo<7XONh30 z+Cw4H8Yq9N)5B;974c=FSKf+QqUReGG-qA2i=RdUxS&6eWKkz0xk^55ioHF`ER3$^ z>}Q9C9h~kLV;PTvekNZ)Bus}*qR zYXvq1GEWa<9&yb!cAF#^oZIC$m~QL~f|;L$l*xJGQyG#_KsZ!$q!5kEnxj|YxFDJ@ zk9i=oEmejRf||Ub<=29*PBX40SO42MuETL7=O>j0vHKaB7%f~$=ac2at*Vt#-sciX z0V2aVu+^4I{?>?3a7gaRUL_IrjC!?0j!03=4(x6Kvwq9}sa^#Sq$ZLqWQk>=hhIRB z4p}EttwGYHah+ccpQ)$Kb~Ik~Pm#~`&mC)ZYeqI1IoiXC*r?xDuU|YC*cc|5fxKz7;0d(ATfT%cze0>f4(}Mb$DT zwZKOcv`XEk*Fjj)>pZlNBnGXZ*XhpnIuS|b^WtqoMKiOA%mOs;dY$e@J`%eV5j8(D zR}dLQ4hM3=iukmk|p>`yp>fS3-tAe|2nqSnJS1)v5l z&9S%1z~}484Mx7Q_Jm|t5m>O15@Ab!=DZB6FD^<%xT?6YiUWhdrB79+PMYSitrn4l z2_#t|xr6i)XQ-X+K!CG-5Ty*Ri3wXN6PoZk;(n(%>vDWuI}Gl&6&zp5k?#qD>p*gq z$C#Bu`ccsm7{!lbPX{Q%;JHILD&NA&o_iP=B{>oBj>@o+6CpaM6G7?5;_|P4NlpZI z?Z|^abXG&o))LDEP1>sF3i#C^i@yHbk(oKdp)zm;gD@|R4~?yR96-Ut(M<@59Nk*| zgSoJHDPK9lkHrfS8)Re-E@tB8&{Kpv-^q?Wru=S10e%ijaf#&m7Y95-X}R=TR+{&E z@t@rad;V^S&|G8;b|Ab7U)?(g;i2t3L&6Ip>6sATHgrcVr!@B4(H;0QZ&DBiTP6!r z;b~w7a}sB!%9-pRoSeral-eZ4nKp_kG42N2 zRKOwwLNiqulk)%L)<CAsxN4`KeM?QiGC0UMqxSSmMk-*i7 z+NSyl^XAIu19#<9TCRM?Sn4&rcdZm#FgJd35WyQV?r0T_&0gX#xbel0_)Db zfUF6e%rJaOE$#8c%?=hoMlUtqs|{fZ;?G=g5|PD(Sp($*PnjM-^QJXPhg?(rEa5)M z{t8}ub6V+B2}~M^Wd_51cj9x;*0wKRU_FHVRXn)$U#9l3$Y#=2dztEHrfx~V^`6=o1v4k9W+D4t}7**+Y!wg?b0N*LJAXT^#8}85y~^g zAX^%w8`wS!%@e8U;%z^`)FrvCt;?v6H%rsEyIVv&7Q&=pQnPEOh+9+c!O4L|wC$f{ zE@$s)_X$1;f$U5zLfwcs)YNJ%M?@Vmfjv&1?VuKKdyrsdDnSd;4lpi4;2+p}DC{AX zE`$1|i(;4-N$N+9*;`~hfOv~|w_g<hM`2<@Bzv$Zj(q_Y`e#W!sn^i}RSf#|UrvCfLU19;J~swJ^HJ}6%Fi;l27MgnWpua2oXM7muSXJI`;zG{>d4$NGR zNNA%C8siKfL1&#G(n38ylT~*P`;A|A=(Jv=3*L1-We9hm;!wfPFTWFW;I>=z`NS$9 zf?Hcx`?^>PWNikTm5zXhkSSC+v)vPu0R&QMlfDO4t~wU0ksTR5hAt~RvQ$AbUqw2i zZU=M1U?xsi0lB2Ep?3N|Z`*bQcJ>4WI5tGNlvotPEckEE2HN}un`jMG5?&2Ug|@+^ z$8luO7xI}hxepD|v4&|yx$`fYEaMF%bVgFoIc#BctZx|ISt3RKYOk!# zh^{V89hAo-!pb^2COD|P33B;J9e>2ghF%zKN+L4ix@05LcJ=j5r*+pFUy7)?h^Skd z4Rj|bBHI6bQV?;LHag9C^=Qb1+ zY2lK63?>0TMA#U6L}3KQNCLJs=&}gYkz8Pyxa=A10{z@rp3GwfOyEomIj8L3(RK#Z zNS0-^b(&j>*{&!nZ&~D8Il*xhAOuKRMznofb#Ww_oFCLP!Eh^XT&f&(!Gm@&9@(e0 zjm3aw!=};4B47~Ta-S@23)aR~p`;aGqlCtz(9--*XaBP(2G81qvB#ZT2)KQNCQ3BB z8jz)8^;^^+%VYcVSOL@uu7);Z4-rzjnT5cIT{{~%aqwG5*B^}K*0qnpKw<` zVWHvL=#iM_FQ(lN{~PuHx-;24K72 zZ3ACmYJiB)0CCBGyA7m&TMd8~`p~Vni`sr2(a{nK&s>_{kVoLRW2umqwj$BCR}*=D z-cb@KVV+5<4+pE-Wo>k8x&sq&2`0|v)@r=)>C^-PYsXq|5QK(SYw<=9!BHUvLfni> zU~j7*@&qy(tK~>XN94AD-<2J86Zxm3+%$pGvBB{QIkge6ck!fzz%f-qf zoq-h)(LY3%X)HL0QeavFer+aHHA|2`=3@D!NPG>cW>eUHIxw)bsz#ZF@6|np^!sJf z=&1&y?rFzdG95>1XEl+VxFc{-A_D2tJhidO#aHe+EFg7l0m*Tt216ySbZ0=!2RyhWw)(P(6gK877IiSH8@xkog!ew=@PF50exprtdlP@v*NYH^Gs+ZGbTJd)fiP1D`Z91$K#30_@nAL>r|B1Del6{m{#xFqV=$$;Yuoo>0(M40KXP2X^oHfF{B=JnOpj?*E zE=9{457y6kSdLc8)-ha?+%1)#jgIECYvlv&H3HPjJz6`2|NS-|S7{d7IPALBI~ZOx zst0w3jSGKvyF38lDrnorbU!io>3TtY4GW}32qFXa2QHP-0IYbdK3A4|28---!`3G~ zunS*W>knA>L(H1$m224vgC+pRYGJqrPu z23(;aZlGY#_lN@E{!-HHLaHv9-vC~w9n68yR?58%ykI zD3;>cstV28kk*CLz`~aDRHrS<;T$?+gKcNi=W|RV&UQv7M?Dq?!RpOlQni z7#vZ<;x4b`K}++=wPGhVVFL0vneVI~1##nXAKlWUx*|XZldx9v-Y{GQ$gd4{CI@92 zO9$m*I%r2bhS&&^?+n@vo^)%AMa+f{T4^@~X$d0p5m-JNaiPy?Vt8*W_qsw7fIyto z8bn0Y4gP5aDf3<3U=^5T4SL`TxRE-=xT!&Nc4_I0*aR48`KQ((bsNT%h7$Up=KmQJ zCz?z<4X+G5%(YT*W_XaK0W#01V@R=dlQ#?&wgZ)DmsY+Y+<1HuYC;n^4l5H@u=W(e z*5{%Fk<>%M+8ai@(7Q-p4&_Vip{&ffvSOX)k1H!AAYWIc2PkSND@x$4Ob$@01w5#D zVT(Iq423CvuQAGs4P+38uxDye4JBhIC}~7c@+2rNW~IV_Z9fS$_lX;dB%2(!Y;I^) zJ!4f@gC<QNnQ6C?WVGkOJc!81J}&K{3di#PA7WcqjYI-2&Pc!Bzd$9uJQcbOg7{ zuybJtaM1u9k@OK&Pf&YmvilpNDS@i8wTKRA_GHuU7b9>0Q%_pNb{prns>NZg7tOlA zt-8f9t6pqZx04B&(EykUfKE^~@$F_4qTO39PR?4w7gqN|d)~yUFZW~)9pp@7sCU?S zAZpzfv6k!!W5Mx7E+rcliA+fvgpO@QLk4KiIF}`x3JCP0#cnysj1YP_xM-KQLl=8F zSrm-MC~|}cEurpqtr!QOh--JSevDUAv1|?8<2G5vHi%L&u>xe88Z9EFnKk&F!7vDT zwfWH(q))dheY)lPv}6_fyJ2Vxpr*b*LqNkWCsiskvPA8p-TxkHU09AP^NR)!6br~& zx@dq~O98|9-xhqXSv;#T?;(Z*fd(S-sRnOy0{wX{k}{b`;U|GiJ$zf>UuX`WZSyD= zeE1f8_!g?YU(~v{Xyfb3(d>+Y7E0VF>EmQhE*5iwz+25Aj5K`Y7$J!6)dgYVSewk! z^7r_BIqI+uy&%49HaYM~JAj)hjFB0{j2P}VLgxukQsxK`BC^^aV%tM%vR1H@D$w37 zXv_1mQWy8NrnQj0nH3FOv!H(jSPpW;coeYRX9(<2Z$CeFx5A*q_sne&3ZBGH_5UsA7U>aN^$@j3AipL|GA?uJO zR9N}L#T!lepCMcqFgzmX)1&$@BPNa9Kx~X=`DIypY`~}mO&TVMHD+O)sQ!WdTXbse zH?+}AsX_R4x~g13;5m2rM^(ei1%3k|=V?q^zZ*g(45G%bRiV1skMBdxD}O6B1FYG_ zcA%TIbg3`fb^*;r5%#SM8VUy8GeqHOws|^lN4-9sQ|$`@SIy~c*xxdpsbf9gfznk^XABf} z!N8oOe9j1BK6}d7XT+>iab|w(m&{6L(ogI*?rkpEA*;8q5lH*7rg2k1EJVeLr@&#$ z7a1Fr*AHI<6hPnrj#HD8)P}D%R9vB;$%Tai8m>vy<#n(NHmSVUk-*XTtPR-7M4*4~l^BKtMltAO~W&vFNZ^$)P1-Hg0!&Ii+i zr!^sbuj96QNm?-l>{u~{U>rEPoV#o6`XA*Aq82cTLkR@(b+D)}*QP!L!A?cqRw1h_ zUO^&@+U(GFP>rMo+7ORSr*?BX$$wZ)5r}E?bPCgByE$aNG>Xvx4(2w^B-1oaf7Y~> z_p+KuYEpAO&VB-qtdofDEzL@()c|JoiEqRsys(bLN{lG^(3=zcuy31nYAjU`yKINZ z6!|o5`f8skC5}+P?1g%tsma6fCXcWCIij+O2gA!*7pqh@`*PT&@d^?aSL$Ki3`w;- zV32HDxU>4Y+d@*1f79YlDZ>HU1fc3hTvZhn5Q8Ly6c3USID9IhFo!9H2#vfHHu>x* z9Bv4`mX9i?Ty^Ac0i|@LsRtWUfwF?nJWuGsbBf#ET5?2Vw*HwdAj~(}8D;=I)?_l1 zd0X#M&CYBtSylllU$q%vOv5a4IUQ^*q(V!xDOg8yWp>-9>fNR$OWUg-s#j*Insp@` z3__lVRAUSY28wK^CL?G)y*d^lpl=*bpa5aZZ!M|Uf&VSrRO#zoD0?N z-bc)WfQl?maI6&NU#uvmu{Imy#I*maeaXZV}2bi$UAl+s^PFiL8{ z@Id0B8;V`yrz4i!Xq%5`@xSH#$@R4uKiTv&QdIzkVJb~^l77*2&T%Afn zl8W=-Dj*t6`Ihtp#jwjYtH{B?T3WjtBZ{jkc*4v7q+k9gzr5Tpf8Q@x`sGr;sAjTM zS=HUc2yqr(tOZpra1o80VC*Pf@$U?bwo=F{lXgqS1Q%+x(36Hc=#sKh&*)2y>Xd{^ zC5fO7ot|YWRhA_M=c{)(R{ErND$+Fqm3Ty~t}REfNK#wN^iK(EET-1P#5TPr)!Mf2 zSB|SPWL%>>O$d+LBw+q3l1E`g3xv9L+?{+tbOH?%;sed|fsS$v*_%kc=BHTD4!}T zmr6Uryzf&b0i{g4UD=#lPX0udk)&Q9%h~)XUHM&=atA0@q1VG<4%CBOU|M^K z;{RDh2Y0DTYj}kfOIjvNvgu zgoF+z6cJjj-Z2q#f2+gRe<34ArI~Fm^&#_o%;iMKv8a|w`4{^hBBvZ-4 zuXf9()mP9`eGn?~q#=`;6P?G68h4sZPbvl#U$8r?ROk`L!}xi2gx6`k^zDd4Z7sz{WiClW2_3ZUZWG?Y zwi4EA0LAj)C=6js0ReZ@FD53xEW&$flm3hNnYMlh7E-GbMwiwT%m}zLp1rx)D<0^t;^U$@dwcotZ8CiO?BUyV`1U!hXrp=W zUCS?D%b&fL^GudYNu6<4z>qq-jnK%pA8phAZvUE#K0@-4E=ZDR#y(;|8G*^{Gl_!t zLHk`K6o5Fw#GyWQ-6u;i4(R!WDSMV*V1#BR`7zetX@*WX0lg(wD=Zg4MATfRD5iSf zHEGq^u1Vd=SA-~I*CbpG#^*cCZIimgq>qS*(VT34RKA_OH3Cjpm{d|Du;t<4IT^}i zB|(&O2Uv$V;Fj-wy2*Y3ujb!7$2K6&+;BmY1*L0Xi3C)^iFY6B+t!+N|{Mf z5gQhFjBWw9G@-~j9b+CKWl>RzN@fvTA;)7JKN|4La<7sW3^bzdTM;jD zZaQuv$)?+wLOBE^Hjd`Duw}r35Qqyp)$Uk%bNKn={3s%HYns(p=aGG;ZC{?o$gl@W z#g+(zSm3i(CRJDLXspaUg4)PRjBi4SVQjh_YC}=yF^;T$P9{#xr(D!N;p}LIr1XZq z1_QF*KeKHrRPB5|pU)oTQ|*m_`8y>Bte_-{Ja_U^C@~rt%P%v8Ut(MwmL{@BeWf2V zWXz@^afNlj4d_sH;HHm=J~d0tXqhlW2Is6JMVr*Unr1Ord-ku|48^p~O+!3mM5C2CT2UqM4*f*$sawGz?ZC|2>BCAnR|zsn&@B5#%9a*8TwN-Z6gOy$ z%N|P-@5&R8thLv=6nA?l#ARM@ZELb5Uk5d%yx(>((8!WVUX6icEp%I;e#vKIf5X@2zyXr% z{zPCdN1VyzyAJr(!^Z{NRnav_qB1eX&r`Y4(46nxbkyeBDP>!k8ej4g`rS3g7^Hr2 z;-3!fS$R3=qXG?wR0=$j_7Xx(b&%w+Zhj__B?w1+Xz1S?BPXO@jFo>T)y?=ZG9~Py zkpSkl_JWn@@$5+pyW8z3hcWE))OUT1mH(&e@Nw)qS^3U%BP+r#GXw%*moU+f#MXO| zZU+F~eM%41iAI3dd&YqU4S~Zv0001cc zca|8oqtdi9_KqrfK7j8v?GoRrxq^`%a5U?X$F~H@DXqyhNg+riDwTxf|8V3=MvBx- zan4&Wo=nB7sg3n_sut(g#VW4#M2;>K4`i&MKS*IgW<=P&s)OKB8cPLdGH%lTm0)^F zFBv*>0EsneEK!Age!D7$QY$5!sWcJvFcfYPH_eNft}0q$dLkRmHw~U9DwD-$Ji%8Y z0_TZp1GB*9Am1vOS2LH}<#1gR;*x_9IfzhdXId7=ZsA#$g8ncQf3JTAM)=KNZ*)*I z#JW+pxx7}QdD|R;$&f1*Wm6T%KO2rsX$ZHi0=zoIW(@G7!6i~x1H^BK za#oC&^eh$66~n|MHH8%Vl4{_3gu=_wBWw`Ti%bI3kOof=CqgET@C1@~{_ZLClyfW2k=+ zX+M57VMVNPJZ! zaw7DV5JLlD$={TH$Kv&9XG@jhj52E6$$I`OEn+$eu5E+Zm$ImIyM%9$(Uyd7YTvXu zKEIFMY^GWD%gL8w%d;qWTe4= z<9?oE?_mEXbWIN2jIF%#*?9S6MCnBnKvH4C+t`_|JZk|^bsf#0a1m*CkBfzvl#w!y z4lQF@P@TIK&Q})a;3G_TuA-DZMWWCs?s7teMY7}UMAYGfoM4ZFn`;U=T+qR|nSUB{ zKlo24HV=UoWGFz;q|@v=x$JS#F0jRysa9HM1t@b;6X@Zn6AOr2kiV~}HE-6`` zb17d_##|>VpWs!+jB>V?hwx8_lQ*np&Pf)X@F@bJ4%)Grh^br$(aCVJg35F>4W=>$ z0yNK(j-zSep_uF%Zca51#jK~A&kMKwlk!=jZ>nR?Gb@6DtCbKsyOd zRl_S(^&1swXu_--pe9Q8e)lNZYg4{8m_SL5n}|Ve+uV^MW=g3W=jg||aZ}>Yg3c5V zPcN8mvZM(W#EV_UEA!Ib3r6bTYFz$oxcnetQ!f7cY2+l^SRfMo+A|!S*S~R%#~B^) zHGIgM4;=7izYKc%ihOb46Q@Bebwj-zn9R)^T<`xE=gr|K67w2;~4s6|8m?tSl zO_Nxd6BrjHK{s2XmZC4Z(NaQjLt zfDtkR7`PjCTckM7RyxQKqNs81N!{6M1G8ig;&E;eS3p4UHP-OoXt7vHJdN@uL!50u zo5$HE4n>{B6YX2d0>DUFsJCE18+8)T@$xnuWr7&uJbUHxq+Z+PFJK`utrgfA&v0>( zisi=~_ae!1#EF8M27Hgr=2<;CpGvw|D6h2$<@K}-U8>&BARPTMAIBR5z2AlwPd(zW zMiRCQ)sWW2#9a&-XE{MX=fMQCUhUdIXgnB}#(3N%jtJOv5jlc>ZlmEi1Q7c&K#Xi> zP`dEAgRZE=GD0E=cN4m>Z%b_^%GCsQT?D_jQV<5sMXU&Y;VIU)bKGjx)~s1=pZvT91=w#i`XuWpdkx21JP|uN}CZ#YYhY%L8JG8 zl)6?M7pwIdjLT@Xo5qC%xOBkj4l#>xV?@KafK$^YVWpJe*Wpz8q-k8v1Sq!se65&( zO%dqdMdc@<8ngPSyCpWgxo*<)VJuX7_4ZW<(o+jUS*V&xX!-*iJR?};NwnFX=7}^% zE-18^iSqEq@}_bxf(qVLtanaNkvEn8NWsj1(FHM4Xau6 zcSit^I956`t`eI0E>J3)_fokOah;UWVM`=ZvJe%liM_T)hHT%fKD<3HWb3>Ik)oK;RP`~6{l zFYo!J!~qCv=IA3c%9E_DJ*l5^J8Y`_0x$TV7a+}N(lEn!ulMCN?$D*>C@NA<)`>I5 z12gjUG{**)F~CGiQH&80-Y2J5&&GG=EiTcWt7CVaQZd17CM^h~I#L-kPo{1=UY^}J z8y&;>z_F_sr~qu!upGn4=7;0fMyfWJqm+-x<6eJ+`(XGH9tb}2@+~jlDv0n=dE8oK z!;-2*CHZak^`}>J2e9qmwAtNaZQv&fRRSDq!WD1|*fiiAywOTJUP*^aTGp^OCD)(Q z|BbU7Cy!yXjn>c`f-y!7Rt}^XrpBnN6FSkTi=&0SuDUO+VI^HE!CoRsOu6x$H-3kN zF<=kXshqy;IQCsB8fO=^zQMQWM_0x7#CiNO zNKR(kqO>P5YDyrnO7_n)c;&^}VZ8j@CcX(=XQDGXm^1F;CykFK48n6NdtZ+QL(ZbR z{1eP7-awpMX@g&<=Ede^_M^Q4)GdO>$tctybF5{$rbMa~hR4Z;%T4wIjP6B{(EAT@ zlAMfBd|!G6wCYpr8`~3YY`61DU=7w4-`E~r`NnoSV;kEOHnyXiYzKe38GcEj0Dr6Z zR2;WY!jV>R4DF@QbW^NDYvuHwWs&l$pTFanotg*UAXPA(_#bWLD)Ujk;*_%`}f>uCoIpGj3_1oyx-PFTnuQO6Bgpde|vA< zDGpCnU7eAb0*+kXb55X649bIQtW(QF{5VgdR_g)+_+HTi?{r!;l!L2ppFQKH4$lY0 zktvZ?X%j{Eg31`PzOZzOycniS=F-c?W9_=@NPHbct5r`L>2$}CK?<0^p8wog1n>bIM-vGDc9943NbBLRq zwXQ`1YASJP1uC3VU=z;M#)%gfblz0jkcW9>_B5rVmRKYXp0`yZfwlyh?A zg#}2Bj>0i)zy{1OnGak0lQbu4qKS(1sV1u3R?HFlZ%))i6V(b-6O{s+CTh}{sHU`5 zifbx8bsNPsX-rgLh1MdYwI4jVNe2D{hHqgT{$=wZVa4DzMmnVA_BWVp8U;OwT?K)C z5P~zu43nS!O1dMyjiDMdu*H3p0V$S=+z|6(p%Cd}vH@eCcfPuTq$2CO7;_Maz;YmP z8QV|iaY{*QTt+KfXciQqPIGDfH|ILvg&4w@!de@>*k4W41LvQnwdlWqXMLksTkyl=#R|~hDcCS%+iR548?9MhJwq}>|lx__+#-C`@P~P{61<2#|?H8HSOT& z9PA`+u#;cwCGC^bC(y$mkBID-wU60^e;~j66ze6e)vNBx)72Rs2z^*RYyVt-XEC`t zt@qF8Jx=4i^=8}9$d;Uqjgxm0aF7g(_Na;#mq{w&l<(H+%H8)9OdKB@j6u|#fnC2S zY+dN7vpVAwV}Mxm_3}GszT2@KJl#Z+kV3{SV_&8JsbPQWDmo}c0nOZa?x zOAWtQP!;eBnonLqt5HFo4lJDt9^po_Z$5Pk^B#pq*YN&vz!^-e;z84D^F*UGaM)|X zLSGD58*1FEq3TnEL?!ZMb9LKPr7r0c6;)%BWC4)yI8_b7s{yiIK1RGZE@1n=cT3S( z_!*4v&TXe}zn?g+>GkZFoENa4OQdi;xLQ2JTOt2naioUfoeW&bI#Aw{ymJeiyN7*xfv^{p8lu;AQGWp%g;Bs$h@b=GcZIYNiT` z^Flc~cHAnb0fa)&Z>J&7=QvSZ#HTYf*E&^9Q%>DfOEWYwYgLThX`|UaRj8g>YPh}F zb-P?M#0-%76mpM19lRbt=ZRvTYE@B^&l_~~&MHh^b^3OGVzn;c$q%gZ-Fo_Vis>~= z`X}@kaO5l1=R7#HHctAI8(M~b_>Qu;+Vk00;S9dr!NcC5*zZ+vbUXAa95Uz&-(4Zb#_CU-84=@Y-Ufi6L zZ=j?3F8}d%(}9S#6O@Qp@hI7Dyk5wk#aXvT)l8u_2D7r^s1|nGbc|3rPFnpXZLG*v z7Q0G}WxtG9%6{d!nN>KY-kxpd8zv0*)X_WC>X z&gs>ayG}8anZ9{nJmln$@W-(uZs*KG951eL@2BR9Uk-~g$%V&+sR|M)t`F`Uej$@6 z#+}GzLY~<+{1`_;?~yU{@+1eL9wY*e1tL79Vg_Sg9tz&i*^1nQwugtuTc5we@C5F5w&>5!a(MoS z_FSm|gN>-){?|h&`Sx2fBFjGaF zkD_6}ny7r{8eI%k=E0}Q>Hvhol+7$}W;?s8XNW~CK|%C3^2G%TMcIBszs`|lFSC)- zykD|XzGSM@2f~QJmHQy5Rxl7feGm=-)mw}U-8D@ry$ZTky^XQLp*l}Z!x?2O7@AZ- z%B7z@N4{5gkP>k%;GhBh**gM75DBs~V?gr0tm9b_ld6W%+eNJmX) zJ_jeSrf9mF8A585mu%{_WsaFh@vYzXWsGSlM%-5!1@_t@B9cm*xEfS!7RlhPRoa?Hic2MOnLd< z>SeG}+4`jlzvyc%t(VXW_i_?Oy%F2h3u1V`?OTinCp0Cxvx2BAbg^!(r}h|>lvSyS zk<>+dDFnH=#T5xlw2-Gz{x&(Bz%a!8wuJK_aO?}SP=GEPq@;XP>lpuwB|=ZZJFg+L z6$lF+kFeyf(V)e>*TwM(>ce|`0`7*|K>PXKrgLy<*>g}7B=*8BdPtjMnrp~neIb#H zC`&^L$xvR}DjVm^`DZ4gwQAIR-%K)fGLhcBK{P^lub_t&Mh>&``LL>dEnMKB|7%7+ z8u3&sN}?E6jD?h;BXQwW`(-wn(0l1-**}rw?;zN&NfmNLU~zsevC)#l6dQ=6;k8pS z9XCO@>!MUs6&+S(=+!SwQ0!P$SM(4Z25iC-E31=e>#7BnNbMH0KfEB-<3|>XiFHI> zo2$oXBE*StAhm+GZ?(GHv4WyB@i+turhZeeAU#?h5O5PK?rdmE6F_0MX{p;>QI5L+ zAT-WaB2&hrH8{MK7m!txotZrTFS_Im5wFJ!7waBd`;LwkRWQ33+?Ai-L zGc(*q#|8;^sj6hxqvOjEHREWj@s;S0Zx?vaFK7<5|D zJ_atJO*?O7_Ms20HQo;`65vup&=O`Xu7(S%^??X zOP5TWRR*fC0%b^Ap|xTRwiFG1{Wm6+6^liHVBW*jWcOhp??xEoa%yX zgQNz1x<;#upaY(NZN%;mA}u^*1Z9{M6%`ZXNTV8I%U9JT-?$DLtUlNy0I(8|%#!i|-cSa5?Wr1EYCiO;|!DKlx11 zj_@|L`@~UaiV3*ML=jRhv9ZNQ0!m;E5#`$s1M?ar`A~3+n1n*iO90L#zL}MP+A~-| zfz$|kvw1wcU(w+Ljs~^S4{q27Mcbglx~K`@5%`*?0qNE7Oxn&zqr53tC0-x#Cym1V zQPN9@mqgi$%#iYDpjkuM>Fg>^U0HIfttv^d3PYYf2ZD~)Oi!Q>qIwA1$zZ%pXb!s9 z$WeNpAZ~3*5{t5ILgaza(H<6vXBYPM5GzctI0N*jH0xqs^huS$xMlCOlP;}8wsXMm zv=az>aoyIuMS~y(v26X!{a)$SOwGwPZL~U#?R`zk83024ys_m7dAvPux*!i;gEuh< zy>e_Y#?|ml28FM(lf~GH{K0??<%=mILcj(<+Oc}ZjoQQ(egLIMzYt!#MHFg!=-`9iO(RAKgvYRYw^>iG)ruYZ`M|rJ1Q}>+Ji0K z^Avj;eJVtDdYerI!o{F;b&EfJiW>w*D5-#AYnUz;;5Y%@wu$>-Uss)6gP6`b%?7kl z0VepAVGV*9>QY^k4mNAhtgu^vhMQKf`B2!QLHj{u1{s1cI0k3|EW$eJD);PlN1IYd zH4Kteke~f(N*!}lUhT%@X|B*;P#9R_WJaKX!NVB|2)4jd@gSyx;T4H*5psQXU^rPD zmg69~5XVBp%9luxvE>(Ln(5Smuhg5F;CF2u=m*qOOVa*;+l+AS+15cHO0C zW^Ll%Ygn6Pn3J{H-`3iklC??FraSUUtj%1}dooCc0S10(|KZYLob@v{EOMgO)M}Gc z#8a7^X=QTijXuGk31?cJPm4!lYg`qbb%a%|PjM^o_joRA`gDEXZ zdXPF$3=BN62xPvouCa5lF4zgYg^I-*q+7up#CTZdk%5j`laIgF8JI9+Sx+cz!zC$c z!DS$?MXM=n$9b&@P|QZ5ifBQVwtOfSRIDAwuxn7ipc*h60&8rW<)B15^pq}nE%GJP zOQr#L`iTHU1G8e!;+S+`JJt+ich-zzwKjkw*@wm+qSvx2Qu(6b6#^BryJBGwgNU2& z)FhT|=&HDzR*4N`!=Ky@Wm8oW8$;8&swjG`8yxiDx2osIz0T<@gz*%L+Cj6RBH}vCMfcKL1<@ z>Sn8w4Y6VuhPqo2rEJ3h5sz>_s!*KAfb+7=GLnU)$#60MD~TFA zjw};`cCxdcWm=O%BZ!4ym_G}SAo?ATF=T*}_LvH2&)&|~zl`7fZRI>^o63BB>Sb{{ zl}y<`-KgCgpK6U&|M=Ty^0zb9E1$<=!g#M_6E~^*fMb@#d|W2A^*tV3EDzoi;pJpS zW+~NhP<|!434Ri@=BiGK?Bz)r zl@muPz}#5#QgIQ!3ay6X;+D*ObbbCAI|i2RBXpN5by`se=ya`{&tn|j%%6iC-E(j? z8Ys&RLF^)S?WI))2lO)F)5KMDI}ahYq0!bA?$*yzYeeT#yU?gWZ&boI)`3yWH*a{c(zB#^t%?{+ykw6XGq6Ha?A?k(x8e$` zj#3uJ$!cJk=nYG8`CNv^e}nj|!Vn{jphEZr;thmrs3wjYvZ;nJbO01>7tkW$G|`>& zN`n|sK2DeZPT^Dj!ENv_$jR~8d z!In%kcd2Quyj%_yGuEfe=M2c+GPsEU@iw4ixyd+X)A*#A_J#ioaF?%7y=rFe{QpeO zP1OZjb8|a*n5bXObuRAr;<;(P$i`;VxoECC*B+ls<}{n9`dpI16oxuCDbKTT`u@K#<}!+nQq-0jZz86o%REbi3mdQ8#w9&|$c^*L$-3FTegII;9RI zN37OlZpX3IOoKT#25?Z$h4PNULiNQ6`)Iy~gAD66M$&kMpLh)*ghS6H7stx6HMA^Z z#O=iNSWCSb}FuXzA!Y@C>8U+|&5uvLs4;Vsvrq=NFHE}s0}_Ng>9O%?Jw)Z~wGm5uv>at0WIXsSl81zZ zY>Ua)<8WKr24RXTc3uV1C3TQ~z!4KYQePp8`4VYqT5Agp{ z(UKE_6lplg)SuUidy5sPCg#v+wbs{Kt3W~8^Pwi2v~eU}N_dBQZQ`H>V+0iaU=mR~ zEKoR%g!GB`k^{l56WYXCp8a!<(=nC-0Q2QH0-pXEcq-%t8MK6;fF3nuwv42#w_yzk z9WYU7z*LnRqVUfk_t|OM3HnORw3M8#^%Xn9K_u3dzQWceEg*N%P6oLqWXt#5bYTy_ zQ@xS0B3<{+m^a2ToRwHgQ}ky07%{+P!T*0~fm%0y_cK>fmo6-?b@Gdj^AsIEwvOg? z_OfFT-h`~L@*z5{&j-w?CqQhT!A^1iKA9GmG{aQ}d-V2+6nfxObaPNNxZ&Tw?=ye& z`P)DD_x?HGr{#{rrix6|DVWJScS)YtsQCv)iwTJ|IAOD`T|Q2OyWwuqtMtz;t6%^4 zlTW_+y`TQ`>$A54M)?>81_n`ihFc(b9NX=R^o#cQG5h=3H2e3ds>$A$fc$p(ke57T zzdoAsIv;?6{C%tY6lyqqtUNnLzKMTSX1A5u>(VE#|A68TO-^q@&A)xepWl1>7eD&0N2~xVa^s=BGiR?KT)G+!E|cI_-7Y1_ zdB_A-JF~9QiVx}R!60yyW2Y_U=pM*3K;>MuYhPu7vZ3#$xS?a`lru~&Gkk+4K3sNZ zkuNN}OOA+y>EbbHnsw6b8e8s{A+O0-bSjAW@eI+P=ykGV^ni<$RCrnmpwKTck?0^l zCR=~J*Vux7Ot=2{i!HP$Tl~NQP_;G`Q!sTI0Pw1_5ylR!#a_taKC7?`y)3U6JjVrz zJ$7CXh&+PN5K?mXY`VGZy>*}yN>4OFFu)l_c+5VNl*`#Rfv z9gbEH3}M!qiDQ^(jm4B)np#>$QHobGQD?|otq{N6B!VY zzCtg-OW)+5KQ?|Lp@+6EdO0E0)Sd7~VN#wY#iu3JK-H4QIGc!#>IT;(PsB#?L~IoI z#71>rszOz4RCnlrB{oWq#BTPyU|o86levmo(A-Weg@Af=bQFXp+ExD$;r3h8d`HIZ zz5rxXs}MaFSjg^xV7;yo%)KKR{o|M49+zMJcf`x06566APE?pJ4Jmo9X&f_aOYj}S z=+gt>EHzR_UG6K^Iya~S+ss~J0t2L(YjN6+q z=``F_trgo6-EXY?h~)$~1y%6^BL95shd{js99_%r&!wM|nzGVQx!p!TO{eE4WBl&)V?XfG8&@+NCSb1eay7z#yoT+73IjDA`BdNU)$9k&s&AE|Z?NWw=} zX?V{^3^*J>*`3J&Sa-5iT6E)ywRvri!~7@Wy*MswnmmhZ_Q|YQ&e_$D68D?zo98D)J3v!k&!1Y?L% zkPXJubKp|`3Dz~$4{Q1*lPQ7&>%oQqxXIIbp}mxTuCd*=m!c;y7edI8-?VsjvH=N{ z|7i-Twb5G3ZUva0xtvWsQ*w7xfbV_qsc(PgTVHQM7T&kH){#w~-($}hmkei^^Q5=a?9l!p2@BYJ|`}8+{?|)f+|8IWy+aLL-|L-q zN0g}RqQE^V1+<=vaRR*>-72DkbyX;=;={`@VWgA%&d>>m6EpkHDD)p@!n%(505%w< zMz}59;xPPyi!^}PH~@?U+pLT|q1#YciC=`q+fru^O-jr13k_)ESZkaEA|TLqn#_F- z=_->moL$3*SP?Io)Y%GnwP6ruQa9jRW9BC7n_i@&FVqk#E@PV;;;pQyF|a_ycTeuP zNZq_~QpTy+5%u2a@M}_y-$bc=9In=Ky^7U;X-XJ0mlc=fmtv_zqMds_f99?CoQY&! zK{c{y?nycMfa^Qb;!>F&Hbl7h-JJe>!tzJYtJIJ{##DJpacRD%xXi0mt#2Huas%U0 zRoK(SB}4dc11<(;!a>+vpG-I*xgz!ZPOg677vA=@pa1J$ z`m5`+Zwf*iCG1E4K7euRJ)eJ@gAtAaqMpY?eP29(eMU8IetDzVR3j}tHqz3t!=e&l z<%;yN(9&nslA)TK>S~-U>AL8jyzyowNG&zh05{CI6mCq8=%(Z4NB`tk(yJkJY#1#q zxDn?$yYzK=24I50`jEpV0M1|+U6DST$QtJ6p#aarO?YgassU-2_k~_H5dAB^^%o!K z|EJ6Vw;{9I9!P@uo5=i~RLW3hE;(M;r>3H~@lS+DFDW&LZ$HGO=o~(-WfEz@C=2w4 zeNXx9jRQ>Iy7z-_f=`cMci|{qD9|{hbsSzeop&D0vj^FPv}bicAqJ+}+F3|@R;PJc zyYsj+ed+w}JX=+CcOJLfs7uKvciz)zN=O0u^D357OtU=0x>}cc=Yx3>a)p7F8i6wj z=B1rlv&V&s!^eqwBz@(Y>6-LbPBn7pDIBh^a~tpRk;(o;QpDMN8QF4UIMVmnNWUY= z-mf1>>7?=Au$OlyHSJY%QVUBfMc^@$j_4$ZIkq)EvhImsGg z!mKY>@YJe?_z=mZRs`DPw8vBtXnhCmF@K{PAGdnH7&~ZZ95j-o@Y83>F%_nC1)5l> zLxL1CW^~3OSy>}g86oT0LUHK~+nn?^?;E!HkX8S1X!9O#^FDj|z}7Y=K?Z#SAW}U$ z15}$13gSJ-RfruRtx^Mta8dsHM8ai~S!|eL%vkmv;cYVN}fS3YI}4HHn!kH$M*z= zzF}RzJK%$DW9sZ{_2&)e8G$2?j2{OVLXqJ=biyX@>^j!XwgjP_q=6thJ~#*0D_Cw( zv#)k)STR6wom`O~Iu9WG9gr0O=~RH!gup(((WZ#(4BgIE@uD@QJ5FaIHhfDvs?vwQ z>+P`gaL&O`+_zwZZ*B`5?XYR-C^P&?HQMq`BZ~2StjMllU0Yicri~IBBqH?(?ru~~ zfJ7?Aqkf8s3_^M{w^{^K5`-j9s2VnT@o9$&psCa|z_rR{h*i83AtX-JDxb#-dBeYe zkR-GgGFDzaJ^v)qMSK8%8f||eIx4}1h=KZ4fPR$|RT+4Nm6|xZUalM;P6)MNP!lPu znd_&1;On4Ga~T1&Tpv8fc_yY7p2#XE&acCVftMe^otb;+KWy@pG9%PTU{Eb`N5Hu< z*P)hf&idj_EPivxh_}|Yyg+Rx8r^**ZOz$*G=1il!2>S?U0V0aTkLr~zw=7=iHxx^ z$&(T1)mR0R$1$k|kcO6OMT5@@Uk0`eb}ULP5=ODIAyfsYm43v^LdHmY!#>;krI04m zb=xnoSZYVyHr)`f=0gLF$zt7zw{&9!vl^uHb>wNl@zV{cVCYh^-JaND*|YOR?9aBm zV+G-An!weR|9|Yg3$$ftUEsMN=bZaE_g3vnDp*Ms&A!J{ZbDU3U=a!`EwyilqzJDd z5p9>VhFLl@nYxilRmzy|38@PyV${s)bjvhR=`qU|K{{Cz6%~bER@*eD6}yo}2Nag* zz=-23O1gzMkoo=o-`?l!TXic`5T|8k^0;R|zWqJ^-~avnzyG(WNtE^>#%0=v$OrC2 z-24eXsKu9cpQP=GQSLp{ax6tp_jWGM98{?N#0<7^f*>9K?oFG}B3P7o)_12}Y&m4t z_Ei%6`K4n1)X-g64}wp_y@(*9dCQDRE0~%lUzx|70-iyg+z==Z6pK^nX(eU>HQjZ5 zqF)t0`8K>R%rl6+T(D0ELbL!(-aTr{Sy%`8B=KSEP@~G@#Nlv`C^r<<^CNqavt-T2 z@5YjN5(toI6R^L&U3oc3k=|kJEJsJ&DNbuO*_YS-|$(8ad4b2_-PWb5lIOm0_Q z8-^5PrcD9c)F+xxx3N*`aX=^RNPEGeTJ#%00HH;_nhD17;W=PFmApxQqnal`+7AUj zZB5!SOrHX+ks(w?3i>gwr@8;%Wjy3B@}K03E#O|oxO-JVNXAn+kh}x=3_s*N-G>gj z9TrjiEJ3wHU`5~-_Do5R$Xo~D3#*+KqOn)=UuZ46nsyVo7 zZSfR&KNj!$CPF}j_YH*LB(P&3EY$_guc6Ysfr#KSky3gHdbe)T9IPeFLw&Hl#A31% zR_@IrwnnhS=8u|p7mAgv`Av^%=zYJ^W#ZVnGQM~>9>T2Qv`3LWvQ%(L2`echHL<imwtOL(!T& zTx=1dv)Fw$NF%MWoB4j8b;N5QeDsgr{Mirw-l>1Cdol7C*p;qpoifue`C~5MRlhJ@ zTm7+UWO;a5UA_D7W!c85x4c(?KNWl(lE!_n(8{&|jXLxu*r;!|z_~w9(0L%Ag6Ccc zb2EH4^78>mJ+*_-9SK6Wu55QjxP?(9CfN+9Z%shIQBH=AL=Bsvb8~wCIcJe+P?&nr%kL_{X`fM9O-uA>_4q%`8No@8b+M|AJcZt};C zqp$I!_q9qdPp&TR*H!ERxo=am;Ozqiob+mbyh&EAA5$@rUY{x2UC?z(0mUbZpY?`c zVn^0x?^&I*ljYecn_S)Ic?_le!8-yDBTQQFVvu&3viovtHzH1>fO=IC_b0_oonwUx z<+?_5q8jN>OB{$=ONNH7i9rK`@$HayEIgrK)I#^#irTM~%H~6)v<2~yVg6bSV7i3O z+eC8IRkVeowtii5AbUKmik8~%Igi2q;$>4Dr3QLc+U{&{4u?{`F;1)+P>gS6vv@(- z=1iusW{pRVJ$%nfvDVIcq#^(!4^Ia7P{WxFP2pjZq!8d)5AIn3qXBrz^HH$3a{RtB z563Mdl&OLbJ%i#+afXaAcR7cf!u<$@@4+rh^Dxzsfe))(y{8-smeCDoPgT)?r`HV>|Y zGu6KbLHA|LoTRRjkORt(%XiRfI8L)@mT|UJ{|!RKmjG#{XdZN%oHw%JE`K_=H&Z-c zM`RNsa&&j3QWqq#q)s%#L3-?vnSMVU!N$Y3fI%DBEWB&ucWsQ}psptO*T$#D2VMS*~c zYH`-FVU4^9 z;_9<$hqm2ya6z{~dENfmql?#@|A_X1)imn`Z8iUfc<#(+Qhk7>&&)m|g@IY^*(Vh2 z!(}rwlgG$hVh>Z+d_-C9?oy8x7=OOZ%QgWxmNzq?447Fh+puea`>=W9UGSt;E*|_| z>ebV7_C>Ghhk>PE3(L0_c4c*5&8-XTEBCw~LF1 z?cGi;qIcD_cbru??H%LsSP48y&F~gjelio%2*ulSqV^aknr8t8@2MY_X zkgaAp&c0nI8McY>H=dX~e!}b9Ns?b4=W5sOCr-FQk@m49tM^b`6v&<5nq&F=Au57e z!NvL8UzC+`RdJ>M$!;C4oD4^gAxHWB*`6u)X9-+%xdTePy10W6xVe`W@B#|Ddt=ok ztG?0MH2wMbu{=%vW8|DHQK$pqS&N;yczT;a+1%W7@eI%7;_2<`g%!5q3|Ec$1u}zJ zp+Ia_s|JN@srh*AJ(S`8-9m@VbV?(V}9X9VGOFc>&*)^{x~d5^Mj0 zqTHW7vpss2&#F6v7xRhuNKIHkjQArhvOD5|BQ_4?2$ko}_wT~f&hKC8k(KiJR|WvE zubqtW>cTrf$r~5B>$p$tI5f=a275s@JOWL#wNxT)velm`=G~VuM<^pP85xXilYmw}# z1iu#Ffe5L6nxfi|CwkD%>RX54vBl;i^Inls@H%_wvS<`>DmYzC=e9d&629>D|B+qi zWmf%Khy*ZbuX=>@Q(l?xLo={oq>Ka$;1p6`h$X%IICMbGuUk=1Lr7pShFqcuj6S%0 zpt(T`aC)%WcuLWmc~RJeTtXcU54`^UMA?w^YtTe`)pVZebd=L}(T;wd@4V*NaKrod zyk@tMFcQ9rA+W5xX$W%;AtImDczPp9!tm@7gk*in5yULOZ`iByzC2ADYzR+Wl8{JO z(=+ql=}~6@8X$BmF!QKG!Kumgx6|gperHq|4V^&un8T1y5GexvFGHY@fAyL9&Opp; z!U;p2?%GPv?F4KRUYj~OLiwm@#GW(3?(st;B>vrP~NFB-r#Hgnfez*DOuqI5$i(Ues%Q-M-Z%iO9WOZw zPzR;{YmOzYg%(-jdPOVjn6`#(xzP*AiVF5;KY`+HMRYHw^PMBKb)LVS?sib@b=cX# z+yRQg6}IlMU{fmT_Ywu7-)UBv0)fb2WxLg5TOILgtUykm4Yc}wW!TO!8* z7Y}S6H(cwGtg9P0+I=@}o_6Cl)j&HJ(2(P_#;vC1u;aYIX#rcAmLzNQ=5VwT@rX_~ zD_tsFMd1b&s=<`A7)b)tQBvj1h#%91h zU4*eNXR593-TGdklPq&JNo@>?ga0w;C^S6B@)8vr<8 z8iqfmpVS*5&~hvU@V!+eC3h^3X~+XWk`ZrI*Rk2nMIuUTPxmOlA$Uzk@)T#cAbb`y zBr>8(oJ}H8WI@Z#Wigvkzi|ZZ0MnqHCm*n{i-t*0O*d2f^NFNd&e-n>p)FMY)I5lE z2$Ii^L_XwXw?{itk9IVin}q9-Gy;k5oZTrQ-FEC|5KAV@*sX7vc+uuDSiGxB1-xZp z=b4z0YDSeiJ)2=1T!E0Or-JcZg7I9*41v2TLtvp)(qYXi4V5HK zr+k7lMG1n26{a`?%M&RB1)?j^2vi74JXZT`8IgM?SB)zkGdZlnvF>nWA98k=+nUgS-)_43Rw&s#oJP> z>FLD>f&et?&txoRIF!eSTaDv%NY*bg8Lg;FZA_W?X|+y4hU0J$3O^S#rw@?!9zhBv zCJl*iA=JMTEr$qh{#CdRX!5=g%c}+Ad5Z?somz2)Wx2IbPQ(XxNPN?gLW*Iaq$akg z`mcx4Pha}!c#7C@0`k%OV?hT^brZ$1ed&;Um^x`6Lrz)_PdjNZNjZ&4dtL?~v*xiO zDM1^Oa+XIZx3k_xI?v|O3@$m|pY2aa0;-DB_BQq70i1l0R4H6~x5!A?wkKP{S3cAH zxV#zO1-EtWNZlRz2d?-H{C7agN&CCX+)*t@yhro$uUcOMWDiHpQ0nhif5%z+J2>~8 z#$SgWW-_|@6_{^`n0ET=G5Rk&5Raq$K5zbWDvE!WDT{H#1zk>4+D&j*(NK7x>BugO z)>C%n=!CnyxTVevTof2%{HNMFY~RMY5fsTL+hrRUIV`&yCdrjNx{OQ6e5tjKeSigX z2(v!{5dHSpWDzDlZtilm-ANIOY-JLP9vDD$MhH@2HdUjS0+P&cbOgn{Sn%0lu$Hgorg*D2#4^3a#rL4}7Vm6Q1m&A~hhfbl^5d*TIxX6DWch zRz%;TR4gdajtdQGaA62Bk?tpO&ZAEIVSEWYV<{+l3wfsqbMV2T$J0`Boc z%fx=Mni~m9td(g=jyQjsIU2vsP%eD~#(9D4E;5DJi)g@OD+SO5LtH0A)21!-3$DW5 z-^G8WVMAvx$`*x;m0|rD{t%~0E7PeP^8rTt5C>+g<)Tso+!1vs4kZ3nf!t;h`#8?gutYQ20pI<963{}97rL=0m>)>96Y zMGXkA+?@3tbt+o^a_&+|2Gk=`KM(F17HuH=lD z!*ENFj8mzhsC-ku;LBdGJpnybnAuGjLHafNz)aDV=dT(iT*A zzJt*NhGU;%W(vuM^1({x4IdPHI3%ewKJg1ry6sBjX*O(d(}P=?m`Bbo22~Sfrw=iC z{WGEyU71ZVO-~8pXg==ta+k8GdBfO@X7P46WpU5Ni;o8}f*4LZ#Xo9swzEWCfG=6( zcs>H->II3yeony$6`UV@7b=EV+ySvSmfbbfY0hovIj*b_`oVCIbyLRoJHcg^Fm&%J|$;?a2+L+ zYcFBkg-~+QQF8hJQA!rVB%~9S?$?{ z(ls9ccP=!zmea6bfLO4}V(?i# z$h`nj#rpV1@>r88-4TSOGnG>AxCh=T?uW&Z>VUuiQxURk8-!mi-SRLe`-g4V`l^zONwYj%b^9 z4NWLef-y!lNJuQQSnQ*s{(98r#XeppO=he0D5hJ(JpdRw-UY8l@eNQxZ)nXPATL&h zE&3sizS-JlFD>f|n_Fk$>*qVLuYdk0U{5QGzde^Pq7&@TE?Q})Xe2y#kz$(1%)2{0 zKn4Vq-u9z7S7dK^XB_AdnwEDtQBIQ3)8mopeHwz;=yexc+s>1(9w6ImCO7K%!^R}3lIVfkL1-7 z3mt<%2zE;~@D+%w*aiQ)dMMD&EE#}l4rU4fA^82mEoAMH@K?m~p<2Q4)}5vmh{6<0zi1CjV6 zgM-sEzTUG^0z`E&$daL@i3tdE7coCaTT>{Lk+H8#xSXwX>Rbeq!N*fHIOr7IH_c1%zp0)KaYP}iV&lY6^5 z6eft0%DZWEotEbJ$U-?Zs#Y`{jR1;Z_;54mMb1R%2fWU>1_WwkJcvb)B`mnutPyvE z$w;;*J_>ly?NxXjvmv#VPAm)$W5;0{k~JRx{HPd^L*7SASo!?WyNH8l&Q@&P!s$QW zm(3?LSn#TL#7++HN*+Y!!qJ5Q!f^C}c&LLCiV!ufs2-wcqf#9bE6|k3oHMT_WP*nR zT4`Yvv6!%Zw)@aD2a-h)T11q})u|6;NfD)Sc_xJ!+2k28sQ(EfuRTScPf}nh6&qOE zl`DGNefqH=DAO~3dA{?KH^qXah8+bcM{@{bBaIcr(O>Tn#9Y%Wqm0F;bVyh)75BI~ z@OpiQ)V4&FSrfuER5CTRXk#S!0))9tYkx*MYGwexDR?v)JQQo;#4xy3M1PHP*G(>S zEAv3I`HNW0!Ek)(^1$~ph9+{W&8-b?V$$a@4<`#K)sY~;yu=h(HBxzQ?a61oHcAZc zow!60B^a*pOfdC`-hkP#50Ut|we4nfnvaxzsS)d#UyCZ{*YbO51eq-Uf+Y*@Fu@d( z{zLyMHw^Ju^8ouJsi&>GF5{FlT0fU#DiL#94dz`wHRrUow#rGa7KNiilmLM{{~~gm zR=gv9qd$An&tB-8cl$8X-aMBNRS}M$TkOq1USnPuxe-X-CBf1X22LAiE)ELIy~44v zmttuS6Ej%f1`W*q$>mioZ{RyRDu0iUS(dyquT#he3+{YA!G%j*2JrlESo^Te{aFFE zjup+wjLX$+@qT*%EAOCrVcGQL7Mt$_niemw62;U2m2dr$R9SF}u9AL@af%LPQ ze(LnY3Dv#>O`4yEd9^BBq`ri6^K@eaV76xt?ByO;9H7Hf5DL*@%D`=nTtw9(?uuZL zgCeK>@u4HLk~7%}#stih@Qzo&e{c2UQ5#H6X_Ul5j$))G&3&GR6V4mJR=sQ`rijbh zaC>S1kPrS@KCV7pjzKr^qIvQSxVXZx)th0^&rW$bic*SOdRYAgkY4I;tGKO1ui5-q zZYR;lzZVv{H=(WT0L)^I6Qo3>1w=edL{R_>nvc5X0aJ*3ONSt_YJ^K-=maS!MXYNz z%IlftCWRkc&88+aIBf#;P(|;oCM=zrpazz#WcD`u<#cj@OPAQTeEhaSiiws=j-NF-(1qVb}o z?8l`ejK6Cvqk&Yeosu=l+5c-%i7WIsSQ%2yGDoT(99qnZueF%Rzw?|Oc(4c_x|B+e z2tOq5uROwQr7_c1{gYuFyO@$L4Vt}{3a|4Bu@-WzN{~33xEHa55DS`yG!}3pH&KMG zM@wY)Y_19ArTM$+3~;3sV>|5y3fZ@rH;EdeL-f-cA|Ddtx4Z0h;v%oUU7rLA!DQAY zTJ^zpvHaj94Jk@_!kU@`hqHrnTV^xKXqA< zxJfXN%MRuc${7wg@+}>eOQoV6$a()B4?vpcIOrcUbwN&|{xMVI-z)P`%VI~6B1{ee zj0{ERD~|~Nm;#irV^9|y?GLl{ZR32{{gZ`$uYZVl1BiX8c|2n^TklvlS=f!RgeX|> zaPnM<4qLL_H$$WiPKap)1|J0jy~UC@a}$w6GNUEL4vLieaDj-=~Z}+?&{C_I*GNuyrM4bU>ku36&axiMC;xS@RoCd>IV|WM|3!}7s=F@rdhSsr#B27d9it=j9#FNv0*?glPqh2X=cyE>}npb}< zn->dxSZs)To*vFoUko(#zM={|ZS7H8ch0BlZps5nDP* z*k_W2H)REM&E(OKWal6@<6$yz)OV8DM9ixcdag|?HSGXi1uILK7GnYKTggrl|m*!fcX7Vvv1^L$ZO>7m*oeys=TM8`WUQ z8Hr=WJEJ(bOK*p>r@jXV?*{NTxxI$AApu@!lvEY<2?{#908kF z@_dB>7wb`OF-z}a1w|K}IsvwPQUdVWKLeAT2}6dROpbMwVzLT^r2z*b0f262M_(Id z9Q)X0Qd)HjNs<=TcStN0iQ|3w#S2D>C@wu67XvBxr;^Q1b8p<81JYAJwy8Jh(-$UmN!lF z(977$qU4t2j@YdC4-D&3cmb8Fdaa<_$>LT$Bp5*@;%p6Q2o6m3hN03pT&D>G;negf zAuF#z)^Jcpfv&3*I7?aUxh+>NsH>Y!5Ddp3z-111HgEsU&;INzR{A?z`n{69-&68k z{r{==|6SCZ=6(ay5b<*^mHj_vXFPrF9F%?ZtFPM$-UMJP?HR%?o48`RnJW%Dt{BeW zK36Qy!xjJbvYIsTq!fHL;aTc8n+(6&2qbm<%vWE>C5!jd?NBI+33qtUL!CIw34%SY zGZI;OK?ZqS_WTw(I^|45>YL;j#GLvjdE`eMkKfjsGgxiIy>M5j#O@w=8M;oJO+bVA zSMSF=tV!Hu0$|TdoA02yLS2||NJ7fT!)6Z|GBrU`#saO*@aFA_JO$x4rj-Gjyv7Ra zjT#gbA6$?9VJ-n($y%{T`3Vm=D+Ri~sog$|nonUb=(xOY_lq(D7HL_lyZYIJ)aU?+ zg+&&fUBa?YER^q3Z)2^?HJ+DT=K_2$Pr~myvVNX~*~l;%sw+axry98-gFHH+rafOq zs)1mqEG_NNuEk9-!I^-2S2ZhT;7joqcU)bZw0D}C4C&VvIJ+G$UMvz#j~>t_TLI$D zX6)F(hS;YbDyE7b8)h}j+UX&ZIkCO=uQ+l62}pAk@%Kap1B)%X%$6+1*uU z%;4F{?#o>-!@BD_X8)2+w~&-|@Q>+zJvj zXa1@TuWIxYvlz30nIs=ieJ)UA9vPQFosTv-i^%L|0SUr?fkJFzZK1F*g+c`X2n8xW z7Ya-aku*ZVJV0AQVF(n4Efm0`1O-YjMY0^HcD@_gY?8O0S|`kg_O^L;2IShM+p)8B z)C|pcSC5;^S?PFqWt-^EXfx6#u8N-93_0h>6{DyjnQGqz;+IXJkc5W>4*;eh22>3? zusH>c(z!u0NqNw!87+XXq|l=jzXWZKrB7~!dK7cZEU*!cS*`v_J~3iG5WM3Jt>;_Y zf6DY*%OB3sS^ivh#uS{VZ%nn8KLt8tiUMaDQ+=cVf^yCFjeapv6nqV9_59rA&zGO@ z&yU^s9Pv9idAQVu_0()6FFc>n9=DG2^2GeD>r6g)#BJBz+-p<|d*SVK44cYas z{LjAxOukLoK|b>MdD5d5@eMwQEHZe#SbL_Kr7l^`Qu9}DU3A`;nm_zq#6`vtv1M~f zq~uvjB8pX<`#;HfLCb`dcVQJJ(IgYn@UbbO$f%D7kXB>_;JiYS0tP}3eFFG?77r0f z*1d-~wtC-`Dd8yuxw8kik`?##Q-0&6_6x0v$cfwcd+)I+z04;X~~#bWP_r9ZOwvvG7N) zP+ImT9iRH(+X^LCuCsc~*H#xE5z_b3|FT+7=hQ65UNA_Ag1Xs&_+Eb4cS-Dv&T}%g zD7sldRCK%jE~pSh0G30R?%XX;(=q(z6FLg>+n6?i_Im=hYT}J)VRs7H2 zL}-oYNyk6g-IHOPHT2ArPiC*J`**JvkqW;kH+?bM^hN(JzTlhFcJlP;8(sHvovbeJ zUd#S|Fq`5gZ(rjdO6P#2c(XTeK21X|qtwM~z5nL6gh1<+h&adbTOqJjML0D!@2p89H39@=1S8va%|2~TSx8w28 zafJ1${LaV{O5Lk%Gy+0&e|CixerWp&PEU|1CB>4B6wM#*J=T2gFS1CZ4Qk4c7@J!i z`RwvwysinAtCD794^8tOcXZl2ViWygy+{djr5*BuPhz{-5;4K(@TJGtH^1>)7v18Y zV`iWJE>`m3!LXyedHA2PiSmh`{l_0>34$X8_$KWH?1ot9xJ6?c&5f0Ky8_6|^+8g+0;M zLvNrHnPpf*v)6q+F`>o zk?jak!Y(H&_mf4Lj%!YYj8t#%;Ois)TQBZML!NJ*izLhnBq0pH`r#oWj~8u}A0hZu zSN(Rma;XZAH|+L<9b&yt?r3WTnD?f-gA28 zQkQAmele8HEk@mJY7|zfo0pEs2%SbTsxOtd5H_r|qEFO!X-(XrdLgTt3V)uP?j%#+7Ay zajFBNY#)+$tpl#*A}g^!&HOq*3|S>E%ozwu&P(`N#0t$fSG#Snz4I`x`R0X;C29!u zgRk5){^X0<$WSknpF9w2E);9xIr(^-UBeMza2MAEs2bdBU`e|wAm==y}$0-zzB1H z<@481s%<#o%VbYwzZD~{;6_DizqXhb4Lgi?v9W-X;*v-ztvK+#2$) zyDNNl_7d=T;42$#h=2hBK$Dl)>ToP~*{HOx+lh;LSxWP=6r4Zt3R{HSHN1d0nUtsZ ziG-(6Bc-iK&-;K8)`DOJECH<{kDKRE*^iE;u2I#c+72!`Vr?C@*CtxbPFkbMqP za)zLj*2NehP5Uzf=>gYb^T)r3pq%I8CnIDeOgO;FKScy*EU2a-u|iaNGL(93aFW=V zgvHYRn{yB;z;@Ad!4hf*DtE(1J7kn3{a$88PSB@H&l#P3%IVCOGYC+A#IF;US_- z;=Gd`!T9);r_1#Y;7N+9X9Q+#gLKL^d9(pH)-yqbCM4y1=;v(t+GUkY;utJ7m>?ZT z7!J-|&5tPQ=-zyN$^L8-?5&_iM*VR#r4V`5TZiE}T|2#T|Hc_6!pz(FG1NX{kH0q22>hd9`Em^$_sQ3J>eA>EFdhAvLGiUbKUjajS)A zAH-sdIt~?B)v^6qw%dch<~eJK>j1UlI5?2g10$-xh7Q8b2ECrZ>((hxrL#r>E)D7& z8rD1TqjbYS!LZ*jU1lF(jbFkl0);P`o*(YM!{+%Bg_U-dK?()c_*17J?NySbscard zPgc_p8|^G+jQr!VSsPb-#A4A4L%URRz;76nE~FBlqr8iwx7)&oCBZ(r2aEu1jK)eZ zZmwPyP_R1Q>mJ&19rm$6(=my6$}DUpKND2eWEuMjgRuZZyjU2e9iJChw~~)u_tM31 z4pNgq53=F8`XYA0SXspp<>6c78mNZ&K_8N((*`PQ63t5aZ1ZO>w6rhReaRq@#hk@P z0NFtjViL3sCy+047L9g`)L~ZwRrz!iss?RP1^7Cc;;X#-Aaodr$~CS_L+C|asx@wJ_F3d5H^G`y zj*r;3qd8oKYP$gxKw;&j73=7F3xjsJ@c-<3Te+wWH`5uP*wZdt+KlB8vXW_v^Y~hJ zNd_Jm53n3JFZ2@D0@9+UCH*Zz@Zibtqq84ZFNu>j0+3M)BR?KYo%ITA8~2aZ*pB?$ zs&{4ei`>cQh`|k`kYII{JGOY#Nx4Jy4dpIowo~rL%?qBQYrDR(T?4B&cMSzJ8L(UE zoj@{)nM;zDymPXYN~>5sqZM6gkJW_CL)m19e-=O;c<$SZ=0{5swSZFa)dynF$AXs+ z6`<7B{K)WNzIe>}U3;*seqZ&^BAje1bz?mR;Ja`#AqIu$TwwgNc4excy7Dt)~KVK5(uZ4;qK-b{&8SZ7+ z+2uo%OW%Myvw7t6kTs>7cc!AdZ(PQXtuK8;eHq)->2{Q0NSPS^NY|=7y0AhXzOQ@e zJsk<^%bL%`O+7H%3)fNxChMo@9{=0lM5Z!|`#$f3S3nC~ML}$AM>%beEim$ub~dHP z*xJ$FBRixICS>1)ypgiffi^~19B?%gB{Unn#r#28aG`j)-cEn`0aQEd<>dOwBN?W( zlLeDw9-mWvFCPREw)-6*pkTJJ6p07ec`T>)?+Amu#SWQLd<6p_OmOYg3g?*W19$+l zEa4*>EJ`BID%7wA(TegrSB4hl|J}(1pc*WvC87;o;Yf%XX!=&sR5aNVNef*amj$B2 z-c=YAf{p?~b(pzdIri19zt=NV&BK#P!K&xv**%b~Nd*#V|j8bSYhh zo^Ynf^(Pm>8nt*=E{5eR!^oBftXg*pVg>g}$;m|FIRb_ux)22fPTqwY=1DD7`b;Xd zCP8C$m1J+NVWv{++}5=|Q<~gVD+vEg97{TwF1s7(prjU@>LwxVzI(-W-?PY7y@uU$ z9-XiO|65Xwi%{W7;_E&_g)`1bguttaQlNo^SMAC|;1vbLt5_apOOmL?T)^LFs3jX# zT1`=l-O`}eT%cBpco617<ihZu2;N>ewT z>I3v6cWu%rmcn5K-X_YE&q}F-Sk^#vT!vWD)+k-Y1|t zG-bin{1V*HE%y{84<+p~>YWoJgk%zzS}A}{n0s;5WuLcZ4Sj}lafgwY-JS3bq>Krr z9k=b_-Ehk#%mHqY`bfiSZY|X5Fw~h?H5g-u&gwF_>R9~%R-%P9OnGWJn!TODG5rUfXq-jv3rs>^7xNWpP+ia$FcNhHPqnDK<7cKLFbK z(ST&+5J@<~IU&^D_0#^De@@+Q?Vy{u9YwO{;-`izY3e|FxSD=EcqL4@_<^jO=W4}Z zvL-{$Rg%z6KvTUUgXZ*`AmQYOs_u%`R*O4TMVM{9u$P1;Q}1a0?R)b2lIn8C;=4ip z48Lx5bFTM><-~em}ZTLM4K-D;Pp+j&&*fngRDRVmgfQ(v%+^m_H$vnPC;9?+j0!Z zxmwX#y@Z6OI-u1uDUatl`Jqu1sH0>8oq|1r%q|MRCKRfeAr_f~LvVf)--ShkSbhq# z0iH7RNmKU{hqvPfQ!n5I?lk7{3QIQ{Z3jFQ4bu;}rscJ>7Yf3&ZOjXijDRv$t{^M* zAiEJpC_3Fndyq26mHjR$Kt(pzG#?8k<P03Y;J@a~y7h z1d}*}L^Xv3G08vz>eV|1GCW;K!~)dTkibQE79=292>^%>pB9PA$)!)_ytaU76oN)@ z#@a#zu(X0`!X#;NWu_}wY({E4&hDsru7-;2ZVw1Ntsu}n?$HTx5Au$mYx(J;^N3;* ziy$o}H(;V7`T!N-=8%d4xtT;3_oG@eEN@MjbVnthm^TjHQceZW2@WLpW|SZjN!lne zbFw9_(=DGSMg_mw%ELAtU8_OQ#?tYgjswy3H?XQiSP)2Bax!XUR7P*2ik(Jt#eVWk zqbRO+g$N1p9`w~NA3;0#f(OmRA7e)Rc78%|U?}N<^ngg*Us8GuQ(%Xy#8hx#%eF@5_CB|)Eq0UW}KYVO%g z4Ra6*YNQH7_@-}i<7MK5SisPwO}}W2Z(vKBU7e2Z5W10wgSP}$!gDM z4FYu<9elv+r&;q){R%Y)R9S{V-A_;*+Ly8e;H+2IFNbT0P>+UWs7njeZb(N&l)EH% z-u{`dzugMR0b*L&B&wijRtzy-=4k)n!c>@>uATI}^+nl~-?9 z`f5U;#8howYSm`^6tNIMOw}yPqYbc13NJ%WYI7it7Bu*?uA!Dt5Mx>o>CEJqz~YH; zNz3mcrJGMa0}0LWw?(q9@qb##BV%5hUm;C8{1v?d!S9cIPhwRjtp#o7j^4>RT~xk! zecRjee17YHvaFn+AI%MigZO9bLVt5lX?$K&7ZtYppNU749bnlL_gGf%&82%zewFhA zFc~6heJr450#mt9h|6u&NnUeLGNt$om48^>?8j}f_VK1d^6=j4lS@!N0E2f#RW70% zb{1RPz(Smr=|Rlo0G@FjpdFk{#mT&DU!Y8pH@p5eIX1P^p@HL|?(Xj7(rDZ~zd6Xj z_LK4fK1@WGdUdSL5E0^}b&4)G=P0_U5-Bs`e5>W+0O@JDt^mWk^({ogjqjm{?Z7>G zqxv3MNP=E~3?xnP_Sv9WaBolbkAnZV#oB5@SG;9{g3u;SG3G;29)};%B!GiCn=^$H zH*;golt>7Pw_n+B7(D8lPmIPF<&zUKh}*wK$Q9&*sQ;V1-L=W#sIN*?}{RnoLfwP(SAB5gNdwA8o5E z(vH9bA>K&1fg8>I5pAEUck0>~+B=&_wRaiUP&PpR?0T4uU~4Cd**qH_7X`q|_MEP* zTa=%;BO#RO>0Ci+7Qcug>cBaxET?h6`s4&y615=zBYWXz!$^{e; zx6Js1!+c(&18U!^BFp(Ak{GA8$VwHk;7uH%;!(|ci4`v3(&-v{F6@mviOQ9Rq)6G;N;Q`8=f++- zD}W9$8dHtuPR=_Z{2cRyU-*|g0b<*T$vA|FZ<$VcVrj%alL@JiJJ2U;tVCcP&RzgC z-iWbF7;4D#Da;_@;9~)V&wDRD z9N!d&AK3Isdc>366RvLpE|4FO42pFw2Yh@JpG9a*R{f9W$@HrmIlty={&K^~3AM+d z>>U5;s3^(hP0YlHIQx5%hA7#lK4qg@O*S=_M%~1{=%0n%b?iRIva;Co@vY}#Z*u#@ zPo3_Naz4Zpt6V$ij}G^ExSD=owaw3j_a}~UBHbesm=i1&GO~3E+YVaMtJ7vQd{0u< z0=tpv(BOm}BC^USL@yCjY!_|GCu}NN^iTIB#`c{88@~6Q6kw7G5lEv-pX~GrfB587 zda}zW{Fyx=Y7L{`re&L10TM}>ptXH%;qf7I$0Dt!Gz55i5gY8*OA!$N-s6HIV&g-0 z8u`*iY%HaS4LDR6wXs~IKs+>A+A?Zmn^7Cv>ZLc-%SLSsdr=#5IlwxNr10u+Gu3Y@Az$j^#y?Tftd@Q zdd0GOOoCtYWUo*7!`wpuKEXU(^bb!ABH_tCpYVq#`l)zwz$g6SNs#mch{O~Abo0N^ z9P$Z&W>2s=7d1TjiuD)kIkL_Z{>;j}KeLd3XU`suXa4P;#pFX-rnJQ)7`SD~m0|=pSjzFb~KR(Xw-H(w1ST?M$H@v%jIN zq+P?5m@)LcdC%rDXKftrv-Ar}nYo#zhpG`;O~IOjBJN?l-EDGE%nos%A2wsBp}}Vd z)wA7N0EeRA<4-IVOTeM!J9EYa2OI<*q7!81Cr3VdXgB;V48_TgSBKC>y$c#wSuW}w z-$H@bmreFXY6s|Ul&^QJ<#*PF+elJ*L~B-kbV#kAHj)}Y7WGy?3Tx6;q$O%$9Y&{` zy-BnX^fBKoBP4@eyCUE*u#6OcEbA|L=#k#I4p;C8N$-2%<{VNH*#mSbj$f2ROFiV191V&cjmmO}~;mSh)A6FTA<5VSqeu#kv z7uHv{bN6_5Y^5rr4$iG45I$S!s)aaNmQU-^yQAn)IGs2}E1H?HXpa3fhTHGnVVX>Lx?6oJe1}0S}a-n)@RhPRR81rVHAQ zx;WdHYe#5QjHrIu9hqC!%3-jxM1RBcH&i`hy+l4{QSc0YT3iBM4UXl2F?Dg zi@Xm6tV=?m$WmzPH;JHB}eAJqM^I~h;Wn5tpDXJ5!p*@sxtzo)y6fZ# zAkF!Y1VphOA7Cr0pFuq6nwS3xRp&m5+2Qz?E-J^T26q*AB48fWYl85_fFlcxv+R0m z{KCr^U2a0hSQ0|rW*QtZ*bOX54Y7!izleL53uS3qYvq)uaSR)yz|#)g`KEMPzbmut zWIa03JQ=)i(0yTfefgFLpAlRfm<9S_y$&xg*~S)psroiV)Ao0W2wISUE_H~OIXy94 z@}S9L>R2`rM9r@6PMKFJ>3US#jd9m7+J8Hs)YaZ_#tmH^bX}be&hjMyu>7o_u>0ue zI~w+nrq14kk-!qGizS7)p98p;mp8I6 zo1br`Wqw}A`Lq1oJh`a)IY`qH63ahmKWtz`6s@>>Lixflc_H@Ew+fR%p_E~kO*YlrRuj;l#7 z3$^U-(>&PKKKSH|@8)hk-97fKyHERa{$3IAbK8Bj%j+lR;-girzgYV8(7mTmpBDA- zi>*IA2FSvHQklqOlz{pzta>Jv3t29Tq$CEI5*|;Ap~v0|^TbB`lb; zhO@AM{xApvlx|4mW#Dx5c%qbqK3LRthq%;`xP!cY#-yff2~L%mk|F_z;znX(13AN7 z#~Bz}Qsug`4CVn6C?P!QTQIN(a%NIu*#26iBpfVw!jguh^dR`>=8HSEWj6WJTVj-G@ zy(}qd!cR(d&W^&4Jwg9ae~gjDi91|fY{=<26a{Kk;@;mfKIA>&S8NlgJ{H?=W7b)5 zAdbbz%O$*vxOk54u3m1eVK*=yttak@@`!*=WCWweam9)abH!kpRyV8hy2rI=^8(in zVZr6i_Lv(o^J&2MM*Z{ZkoMm?cd8wUT& z!n>atuZ6do)LHOmy=H-R#WX&|BZ?VF34=m`PH{zn1)OUx7kBVL0V6VUD-dlP@WTNb zu+7%x5l;``5XALt7(qL5ee2?S=XWj>Hf$3h6u5L9`(=~(ae6kr=;RW>c+!+7>cbew zNo;X+j6LV07L5aK9T|llUfGUb3-}CCl?XCVJ=?xEcP~a+L=u6BAOem7&#OkwdR1sI zg|72$rj=f#YW3MJi`B6C-+ut0>sJgU&zfnHMf`hUxaxGou}dXl<5Gx4-NRs37REEv zs>+f9f9SZ!MF{wlcytm&?XK`Ay+x9kKi{Xhz4d z-`)YM@IYlc#K2iUUUS?yDI7NzsX95{Q8z4O1HLH3yY4)&R~PrK-RHiP)Q8QT1O@{^ zI)n3-+DZE!ThFbXhb(}rF2#<|?oknsignPadsN1wvU^nGDvU-3u4>^PUZ2c|pLH%q z(r>J-q@6*!=@+r+HWy!v%vw|D*I@hc^NvXoBGA#hmRK~~5s2QgLc`rN+B=kffbqu&S7ePMT-H4Ed<1@tWeQ?%|Fe1Pa43RLP{9Uls&9zYL97nNB$yrN{-MXxBq+oeQVdmLIRAVS zebxYc7z=cAElYMemh!dg-((YHs_y*)QUJKIJJ{~W+>qX;IOA!l@XS%RL zG)05eJ)`1?w}%q!xjGoGJqSMEk*A`ZH-%)FTJb&K>q>ZITMsOhsG-;2CFcXJg;~xn z+qx+{&3bl0X?!0y6-SCA&0|kElsLkrdAz-5%A5D&Cg6YG#o#H?JQ1&Lj4Si?WzEJH zO8<{i;U&+S(_i3S^T$!vVTU~H`85CvRr9~iroMlr#wa%2G;^UdQL|;JF9X+1H4}>! z&e<*(SmGjW4siT?dTsSns$Ec9{X}A~o37L-@eS^ht)V6$#&GCXzk=n8eaEsSr|0^D zbZ>w!$_A%ldIFqvp;t}a#C!N=V8U>GcS#GFwi}qXgLchECLdmcog#vvb-m-J_t6;8 z^!Shu$jHz9^sgV*0})SSw&;hLrtfMP%>MqGU#=w_eV znnwNcj(Uvhg79qA=k0WZ@ybtEirIMk3pN)7!*W(<&8G7--qqrWcM#r!j{u!|Bt4It}#mdmFQxT#wnA$uxH?97@ip-<3GjT!B>v| zx*G#f9cBjAsS`*CwoBGGft<=qBE1NCvYRjtB1rR1i%?+!Tt2;f?ygpb8dB1qBanf+A6l>OA8Q4@hq6x_S4n(+Np3)X{E}%-_sjsfcRh`WBY_vXv~z1o0bC zXUPPppvSv-f(qhQ_YT-$4C5fjZNPESDLJXBX5A@ee9qt`d}A_ts7-3W5?b&Uk987} zBhOWdDLl_U>S@%xF~{)Nvzjn6&UeE1{SBP!{;eKIV8<4!|AAx+%-NIzJU^;9f3=Qf z78XDiTOT6T3k!%cm~%vGOi{issV``x+(JJ6sk7^24E6KoQX(*^G*Jw zXh8~<%w&xq}aSOq_fdte8@4vYQF=E<- zewL`M^c-*mf*L4@AX5EfP3hu@4Yf;cFrw;PSxuClnqSYFe@96@7d4yVp*6W3e{S`P z9fl!0Bx0!6=-+S;B;j+5MLZ00(}9UJZXZx1!$571UJl1a1pki59~g#|0%B>eMjS{B zC=DmVX2A0wjbOQkoTz@d^eKbStM7_;!!=`XE9_!iC8WRgT+9fCG%(@-Afsv>8}p3Y zW!2C#Vjmn2=DdU7KTO;N?P)VhM|!|$;Mq`H-dX^o{Wvr8O(*_&gde?1NB98+bwldp zOLpN}JdW=pZ8>v8|1mFp8auk6$@cI*UkGV`Br=I%l-i{4P~xK;$45U%F<#NNoCfrK zBuy;FL(sfqDB3Z&H6X?S6Lz8)MbVCdD9=q}U^<^-46*NIHU{nPv&KL)14srCwirX( zCj5N`fdBwvyWVyKP3Sy*blX#?lMce0Eyls)6-xar8OWG$b2hMQ(2kjB- z8)!X4gL}Gzj9fUfM)b&d5NYa5 zqWsQ!P@er@aP|XIc5JR=boPU}vmdal+wLiAnBlVd#ejxt9+RDVI0r$-P>yRd;2J#^jbV$=M}CK6+#kl?>y>k5;H6!Q z6#nRYF+ofWHA)GBFkZ6@s_U5VXeo_{#-n1G;@gefqAJ}gMAsm~QXy4CBidbmX zO{RFG&u6@y34`L1WGRZ~yr3nCqkY#GlAz@A->AdE+Vg*k$A9pFN(9m$7Yir@Me;>$I=?qG^oN12-jPqdcVeXSZ^-Jt95|NI!N6&ciNKc)RdG)Xhy zOsvtT5!8T@rQr=k1#RHWnImqux_+`4e313x(2dMvxXqy&SRE%pP>FYYRt#e+j(~f4 z^}>iSNUr68%wqGYOv`AI06J5A4*4J8uVBxh(XgBIt^z`tc#7e&HjvBWbjPEO2N%eAx3IgXRD5dh1*G??RQ8ccT7RYcj) zIAL%sEfO?h8caejNl8qy3Whq3>`D43;s8EDCZWK;!6Hv~saA*1E{?$6fv^4wX5@W%?MlS^Y+? z%hti!pLP})tm+6uzawOCA216E298h|O%|F|~ zUiY{QGTiL-JVEZ@USQi;ED>g5F}bt`>!S2FU|H-KN`@_nG}?>ivmbu!zVdnoR5btm zM;`sN;`+#Cl~*rja4}?rbM?~@4EkCGEOLR~0p7wsBepzV2uox_*xEy-Mkr=dn}!=d zkACluJi_bnr%u(3Fq&*YZCsH(Gnu$aA-eFf?o=O2c8e-JBR;paKeuOa`_BdN0u_U%n*q$6_W%stVD@Yc;84L?OAi3sKmeErGXQhb0hr4(T85y>6u>;2 z0hpTv;3)7E0KT9Q$}Ipam`74O(zYN`d z6;a)Pxrx3YZ*YI2LzD-H&+4QW;F^li0C@!w&(BMqZ*3=^RG-D2Cmy%4{^i32OylHG zZ^J*sV$rjq#8}}8)=BtA4$pz@^Kml=>j8(T41POEEFPsK&NR%&@6NHo6<{lcrP~&j zfMLzy+a&}imrP3dhNn5tcI@Rz2J(ZF6r$7E4s_{RqHNUW#k3I$hFd{;NR2Y&99z3o z5wv157#B3Qs%O00#Upr<)MVsJO-CP66LJ{8`Ko-Z?#QKhGhYnD z1Wk}f#8lsL+~1e(MujhLpA@&lJxW)%QQK~#Yeqeo>ww3PeX(?(2P2xt90opA#AZ@T zeug)3;+(Yu&AaNs_u#?}Qzs-!EH%Q4@k0XisLkD9TH}5x8^=cX38ESQ^`ohii$v4A zcvKu|SS&v|$$(e3euA%xlgHaTP8mZBXADcX6T_4SFua|hIUcetDz7t#RNT(W;3S*g zb6R3?);;-F+V|~??o&>)PIY~c+6VKR*SjXvOQp>EVa^#|N3^T`m#se`0Z zg)p2Iq2~{xX?|>N$r1&~(Wqf??@s_%Cqs)vIa~jmfGb*6^Vr59uqdV+K3-Om74KzjRWV3=FNP<^r$>8HQxoh3OgiL~a0W1De50)_f?& z_fqVq4kk?1ylNmj;+YijI@+bYJ;2w=X-<$gre*Dp3(v~)(kUgRBu4Xh94Bga2IC9}JMNZH^|}zKZD87E&9k^9V9|8m^p=<&@E9@v#TOrI zsz7V$z$c%4Ry+Z%L5O&=u|1xM*e4195jrR8>;&XNn7e|#x#XCc1WuTKi}1tVY`8Fv zhz<7#KlCAME*0BuU+3?$uG@XGb)W&MZZe3i)kxwD8+49Xh-6L*>IkQCOdtuyn6JEf zrsT)8SF8=}W)6nCX#+KdTI(VV79e^e^#NPIo}1D70;6#{vW7AKO=IPXYaEJ(@<<=; z5J)eD9rGi;iFlCEZpIC2R+HHl)Sctj9}v=F(6TY6S~!6!yH@aNda*P~OM2%ZCnPGb z4CKUI?BSe5k+4l@B=nmhXAg^MDxt2pzZp4UN>koiC=6Q80wOX^&=(&P{ARyp7FqBL z(lUX<@|c*i;AGGs!FE35ZQ^c9Gpb7>mkQ!SfVIrePbSnc<@AbQ1&Kk7xweV-!vfoHFdv@ zi;V|yQEv>qr6{KQ(y`{&x28r^xJ z`P>_ADq?OQOP7pa`Nl!3*eW&K+u;vXqh`+DyI=s_;EcZ5c++4dD~|v+;FL`;)sKh` zzzq5DvRI7?l#yg`^8Zo6jGem9Gj)kxWH5e&P*EVp0yH?g?zCac)`RuqBxwTH_1vrY zVy@w!QrXOK%Wls(S#%q&VYhLn%-I}u;Z|^~5}8!jYlc3p2PaZ=D7v2d%yHIr4owwM zll^p7h%lcs1T4)4u;I+Cb^DjE!^zrCG3*RuoMv+BY)%CyC$|Cglb%z4Zk<<`#ln?P z;A9sV4^vqw4I%Xj&)LOtPwA4J3U&Udfsd60deZj5!sK1m8%>@|5@{9e+rL^r(9WtpXV8 zYJb!&xf8Dl;$}~Z8wwk+$^Yp;KnP-N9#6NK1jBV|D0+RQ!>C~oqo^~GBPNCswxM3g zMsUeRw!-L#bmsf}T7oH^c9cE( zj)Dlee?~nq?Bqf7xR2r*>BN|tIy}zO{#>)FZgdAg-~?Pc#5Ry)T#;b_6~GqY2ljHx z42r2DH8|F+qDD3!r$_G$f}p$_KPvQ+>EUHrvBrtxOGO?522B0LN{6&MY+H>Lt7tWK zTE5isDQ5wQLTA)Y1q>nhB6V)rK2w zJIo=@c#=KTSP|_=LbtYkrE(etusRoRFhX!CDWSr3(2h_BEL`!I!xq?%G*66ift;!F ziUk_QHuV0$VP`RBRt{T*9g-+pLE=ZMm+oCxh;#z*oR5Gyxe=^Lih{QfK!6gJlNzWP zb!m=nc#;lM%J=gVFkHP@gbF;m&SSE9gDACG3=jX8!tbiHWPNDoiKt3cwChOoOo?5Q zW9?iy+!du;78MSve2H$r6lL!8E;vlm6VMJ>rQB3B1TYAy05;5~)ed34$)t?N_kiQA z3KrGI*n_Ze!{(}st;#sFrx+2zJ*mNt!>VTg$RBsn60**Xa? z<$0*o^h?l_i>`Yi5~9)gLkrQl0ntVpXk?dZahsK#mSA12abDn;y$JzxPPJ12G-v`H zZfd5`RG2AYE{oIfOBw`GT!Q9j$(GVj3;*U>771NAHFmBlOcWFZd||3E**T`Fumn3( zg;o0Kg<*vADfBMu<|S?2>oj*HtD{QB7t;eALz)0FF`i1Wx8fX-BGhLn1ob&f>NEHQ za;inw`2u}E+dc#;p5!N${5 zCQOjBpdu#3Cqh<`n%yGwEmIVN+}Jcq1P_3dgV7_|0IZQJxtkATU>wTWSDYd1-EN*u zfOg{VTzj?qSaX%C3-$q|hD)MoIFwrz^4>QME;mTY5HpxyT+jQI`+1o6R>JANHw}^w zTCnc4k^%Q{0?9`JgYaf6hO|&(cZKg=1tsy)7Wp+^hOq~!)q0s<`9RB-{@Umt-f2eb ze9~6>>7#$->xIy#RDe$blB4fm0fP_`dKMLc5>mko{gZ;)8m=&cv(eu}`a+=k0~>77 zzuXG_)m~ckS6pe)m*=9t)}rW7=@$JXPMcMH;RE_(QYJVFJkdWSWDCStKyhIw(0>z1 z1CvE~lu-3g6%8)GOd}&S&6&oBf&oR%wagt)JgD1e?Ti#E3x3O=#M~jryXxY5j!#^^ zQ$xWe2WQ9My%lFi@p}54ojwhk?IA+pE80%25&#l&6GY#W;!_aNY+6?t`b!p}(a6Yo4DF$E&3xV@!dQO|{ zcPnH^^0Qz8&lyd)3}hcnkv;T9d3_H164^mv&a-H_5F%SFS7x7!>{KSN4-$wj^pHI? zofyv4`Iyug*RmK3-=2e)N#Rm%ffAeXvc<~zz)IGy$d9GIk&1@C5rT=7TJ~Oj1A&l2vdDTqu7p5ou*UeaFf$CO(B+3 zPa8SMX)61ocWNX2^qi)$<1L(~Iu4blBpN_g5wVetKj`8?U=27kDQ`oeMW7^iI+%dE zfZ0EZ&=PDweK>@b@l^B2Qp|qGV7vuMK|h;BC~Q`6I*!_@mm*gfL9y zSzDgpQ@3>=G4a%SR?Dd(I>S1ONn(2au$uTJ8{f%bYG+klz#se~dTF5m-03Iv;Z`5X zyWW3+Xaky>g>LpYw_MPYP=9mF_|c2=rFKPQQP?ux38X9gW_BXI9pn|T#;)^l(uk2{ z5-}kNO$mH2_h(<*?l~QiBbd41LsJiZF@o^HQ12$J@U!00%sUlvD3aBJqpUmB15(pj zb)chpi4?RuE+=@cjUzY11VJFqx`_R4kf+xAX@6DhU>I@u!$s82g`lU>U4(jZwW!Lm)YAVP z7CO%u%16=Qn{5Va@6VRO1b$*q0k5tRK?wChFoSsigdGMFq_nE^u{2t&qRR33E*mKW(pLwFJDK0babc7Ev0eD!2Rc@L3)%?CaP z!|8^*!?ZRGuQL4xLwsYg-aPpQoPNkUe2Mh}XXxdrC z?_SGa%>W1$(v7~@ZAa%ks0U#GoDMMEj=oYhX-oQIWOo8GpG3L0^Qkvo-#`_1#C|E% zitzg#d`$5p7g+ov36HR?1N?-UM$^sLt{=|-j-6Hv@PNRD&$(%mo0E^&cn&P|00;(>W7H44cikp0U0@PrD=5}3K2Cm3Ad zt9+~_c-#mEa-#%b@9|ycsJ`m$Digov3)7M*?}u~ z@G8iSVR|f_rI(rdQA4EV4tW7pT?4BYpar~q&kk^W2s3S)pi=ND6>saV=Ok$P3aoVg z%6zpcNo>_bv=%iSf(u2GxR2(J8|F{kCgt~MSK{Hem(qHCUZSPt&0-kg=|h@^VBkG3 z%J%U?W=0Cu(&K^Z36b+lKuZ0^Encxr4y4(X|MxWTL+|HdO@>$j?WrIY;l} z7A$c#dH}pX+hK0L<6!R)R>@!Zd*h-b2mJs#RSj4~UcwVCM~s$6<%@DCYluoyXNo~KcyFPY({_weZ)x)0CL#88LbtIOI9BANgk)*r^Ip6@C*?{W9!N{EqM8{w5R_}p7fva2}vWf%D!j}+DS7&=%k$5wEuA{#bRy||4Sj?g@h@MDL zgP>s|F-g%Vu@kaPj%FAa!}Lc2pyeMY8NZmxR1SqwT#kRS$cHYYfblF%sfv2Rc7k;Rztwy}xLb(Hjg;X;**K#{dm>4U z_T(o;djf)Pa}H0^<{Y3Fn{xt}W{JWnnl|U;APtNkWu72wrZJ`R9&MG%o5pUG_B1F$ z`JbioWMkiP&xNg~n} z#MPQ&lL@Uxc>4foQA@G}YVyv6);|7~r4$o>AjO0S2*W>hh+rKl=TOVWf$52|CA`pZe@4{`?=X zjm>cQoQ{fJs7I2=XLG>;X>%zAnM7s?g)`Pi=rD5j6< z7br(yERq0fpnP<}aS5G_^fa`YO~z`Ki=e}h43H!UTkJZAM%#8X!kxRG*+KWvq6Njc zVeN!siCsvTFjAY39Y#_S4u3h+K>rWX6(HI$zL2j{9#K+J@lk&2Q zy5t#Amndbbv)pvyx)eAo>axEz`+}k_dt0-0gHm}97WD`O62H>sUBneYk{f^pQo)t^ z6>vf_h^wST9?z5oT{D&So6n*Skj7k~!rAiX3X<|LwNWK%YW0pB+Xm~aEr4)%G$(>5 zQ?NdoOEgB0W8)y2tM3QkDng{W2CWk9)7uY`7SVfBp%+$^#xUate2p13?iLw9iZRvokyJ!MB9X!V$(X?iw@_n zi~)3EeTPEK!9?*$+ni05hJYvSt7!NKX9_Xbu zwOTxeow;Md1j_B=>@WT))Co<0jTAj>-VhnFv*wGxnjYs!I8vB#5LP`L|LK4Yq=ZAL zq-unSRrn3OYB8+a-KkvxdLgW6*xjA{#o6FKtPlYCY_L^)*cvSwecFjjxmZ-c5*tFZ>Q(}m17PS3(eqteiXflP&%L<8tZVkKvgA<5+S zK_)N{_O~U!U!-9i!C&y)X{=4pR7Ge^C77IJQ=e4BsTN7wyzj#qeJxjts5*R!Y)G+% zu4>`;R8^;Owk@=$wGlLoY-WbkL2sY|w5y271X92t)OW_(ZWust)rC9O`jU&API7UX zNSs-d#Jj>X(M5@F!$62GOU%56ec6@uiTpy2_Na^TpOpCmUP~9c@q{p4-Pt15$~eG= zKA`XBrMGFUqODMrqntzC2pP?NwvyGj=qwK8p$)RCCmal6>6`B+4HB46AIMcY zeI){nAD0Kv%WI_hwJ1)u zQr{qF)wiy5%5S=dvKMAt(%xJYm99sGh|=Q``b?+04TOf|tEyif;T?#=7BnC6XNlre zz6IC@}gMnj+9uxecblmw&mRIbk9sfnac`Er`vR(E3!0u8w zdE$%+|9`Xh?tz-sb-n*u>v{Hl?Y-BY*)z;A1FYv^kU?i)G#F?GYt{rfDkUZwrJbWE zJK^lnK(S-zAPg4~4a@6^)C%)%VtC0cH9^JHw4|)eD=HxwnHrrmQP1oB{(RT-JbTX` z1nuui3bSrv_Bs=LE%V+mcl973UQu6 z;Aww8Zi2vFV1*F>LAFT}AJjPLw}v7a=PkaLANi2h)r}na5KrK5)$y?+ZxUu_{={hv zk~)L!CPsUAuei_1Za@~k3Kh*<)5{lxl-HrMO$JticYV%1$J*Y4<%YG*_P+yN1{Jyreu46os$0!qDx z?x9`)7BcPzVBtD}g;#PyWLqGNDG0AZO_}P?P#+ba*2fi%@7^Blj7r#*eS22Ol`X>( zf1z4bJ1W>q1?#q8e^Bt){jA@`EO=^v;t`PCntJUcypS`^z*gY(K25-u7XI}z@cS$W zMr00&f3gLym`+4X@W3$_yx3a2Jn!R`pA>AnQdIce7ptauEUg6|r!A$te(^MZ!1a^) z!I>f6aT~~w)<8Ix=n1y)iTTk!je_c@2W!u%iRi2P^9s$Tc0vg_x&zJq}67 zUn67Gs77bAompLzbu6O+eBwXWIz+4r@N8{ZDO4=g+9JAz3Sq0}R!#jGEmZa_oBBOV zK*eDTJHZIeyOG>AE5e<97`}BO#`>JU0h=7)|@GWrkX&O8?9{hL=p4%8a z7Yv@iG5GNq{CFBX3&zDHXY|Jd7cgS%9xRKjpq+g`NEc9S?x_nT^@}<^3vM_Q;#7e% z40ftNCtBocvOjNXPCg)MP!iZsSWJc#(v5jFy{zj7=RZ8OHUEGMo7ZMNHqoFZ7MqD2YT8=@(LieCL=jLloh{ znh`e>^ogy(w}PnOlg-sQ^WZm9$k^fgAt+=tpelh$AwdZPx>wamAd>&|T|&HMAOaVy zQF#Vyr?Ax?wy8IdfW{MJc%{D0iUo^&DC#2-xK09;R|wF$!-f)rN@qFNXi({_D(%m* zn?L{U>(9~vcELo=#RP3I8aAHLXhgvY9wJqMl6`NW^yei|>GH!z1?qXA_WMN7=eG2G z&N6!Da0=+Tkk*&j>ik)D5zrs>Y4$W<&R*xmHm+npe9_BgeJF`_2ORlpATNJh~Q@{N7jG{@#s zw8<0w_muwlfh9bi4i731N2w*UAJP4(q`B@G8$Ygg@r(^Gj6L$6kLB8OFQc|msvJ*&&;hvCkY(&aqS){3>rWH~i+-?yO z6mF+(Y(&a$Xt8gx#lF$Q!N|W%T6ZT9Zw$nfW0T%XL}FF|RDmBKK!QBI9fiFi#Iph1 zDXriy@NLBfmtt8wNHT+a8(nOzoGOQ2G*`lMIfq+F%iHg4FvW57+<*p8Fw_&xb0hF# zf(yJ6LQ=H?FI5S=MSzzP3}`+=1&p$v8PsGz1g<1d(-XD<5l3yQ1P@yaYS$iAqTRJt z(EDTyagua~$wTeQVUuCx1dD*L8ZV0gekQ8VL?0%1$I@dW25rcg5S_c{1H^D|bA2`J zEjK~axnMEUTkSLzOaamu9qzrYU=r_xT>oP;&+m}OIdG8y2d(!od2z`qJ7|TjZEafO z!dI|=8S+4Bq?h{CG0#2m1UR%40lAN<&*(0gU-pC{oJI6B`R)npv$oN@Qr@RQG@pBR zm-Ul?keN$2C{RX2Je~ORx5qYZ;n>NyW;qR^{OGW#R{7yNAu_Lo$fPhFiaqxdoFD+v zUG>WTcBFG;XpEi#q$s$u7=euqXBPhR(cplTfnP40c~9489M?=bd{?Kt@ry|+;)O;p zy1qJtKqQYrIlA3U;BWUNZ9vsRW;b%)utpJ0nM1|7QKC4rLUb?<@c~v{>2*hTz`(x+ zY>JUne_0V5Cop{1IsHENml6S~_T1m`IQN%h9Z!Mo5vq0fjs5Xe&-|U#nZGRCer7$P zGk-bM=LYSWsQSQ(8@y9Gxkd`I(hZizj_fe6dl(A<(1`Cy$2VwcE7uw!iuC9i6j2bC z7AO9q3F^uPrJ-n`%NhiKx=p6%6=8dkD!V8csC8tt^U~&64YJb_5_P1sGcnpBl-Scj z&~JOTt(__*c&NkVY%WYHpv81Wd`M6Scbn(O{*PMJO{!k=2?&kBlX2z()Ez#0Ic=Fd z>OPV9>n{Fkc5&vOrCgyvdf|q3HNlcd-`6JCe7JLo=C%NN#4Bifsh8`HPBpJM1{&}9 zM&44G6(#E8DS;7yGvT=PoOWOpW5k z$g#}gtUd_<_wL|ujR9_``4%_2EpE6^D{w>4nD6Ddp@+teQCy@w{G1AUfB<)Qf}kvT zEd6nl0JnM!eLD}(UU$sHZ3YSCsMmdE#cTFZR(UdU;YB)psL{5#j^m--Xi_MI1(jQ! zZs*dw1Z0ZlG2vv{fsM?)xqzy=JA3#h*jm!j6{<oWoRhgQH(HwyE-GYJUIFLp9iyim#K{}uKQ+3XVV?Z0XX&h@EibaCv$RjF0kU% zZ=aJuXAhle%|JWT-}BV(x>zyO`dTy1NQX>7>4BQfF&^vaXCR{+c123FZpyhklg;y# zSO?OrZq3vD3?@xUQs5SE*juG1tzT?gGb{6Wz-pO{!Kff5xLd+v){JXxqr6NH#101M z={RVrC@Y;*8c{PbbH(e5qUHoq1o`u)m-=fF?dos;NcN@d^=f`@(6*U1JISVft0j!+ zBsfJCoH@3(xoryc`05l_Jlf+&dzSjgNYz~{{ezbAUJFZFxK_Q)gVL1;_k~0Bv)$)94MjQ}DJng1*)Ho~;=OhaU~esw>AM zKXmgdff1cnzhXSi`m>&f13(PL?TdTdD_HXBeS+kx)u5VFF953!!aEuq!~D(anowfS zFSz$Ekb~3Dvv=kF<4)~-1AN6&VLoCqPnW}-81tp{Rw?mJ?H_jK?+|$LY25g0o;)J0 zM`K#zSO<<;Nl3{l)~~A&q3xV9^cc+)0v!9RGg@BN#2q14(MF)S=~r?=1EU zV&l!nKFO7(xt_7@Dg6{A-Qsxj;i*Rur=rI3=Jv}SUFLfY2(5yjuTBXMxi}r`yi*^8 zXNmr<<@*w@09|t8Mol6aMv~Ie-r|q+ZIVLW=4Mzt35RRN?N1jHLFp#|wNpDAAfGP$ z;#PL4*Z{v!tJ;3_KmYwF1Ofse<%g%1`u(|E6dUG9-m$Uq@rlW)>6vnNuAHyRg*9sz z*R4Nd!^R^wZ9eMgW9nZ2*e%E1@Az`-3HN`%w(TdLwBvyf`k|9gDR-W__Oy*ZyleOA z<%56Z!KeJ_o@Qsc_s7mS^Q?#Ld+5U+`tY-V{3jl9PWi~6eAKy*Ui+AzI`1(*{WFh! z+|NGxzx>z7KjFXq+!KHPzyHGdPx{&YO9!6($fx|`1s6W`X%{{HX^(u$lg@wAFa3{a z{4zi1|Ihz*@iQ;E^k9BjetCXHer5iw{Hpxf`PKPz@>BLa`MLS8uDS`K$T&n*6o- z&e!SB>+?6{zni}?zdnCc{(Jci`T0+JR`H}qzBzwO{>ZoHZ_9r_f4eUJAb&?*?)=02 zkMeg?;$8W>^FPktli!#>`q4M#@6F$rzdye@{{a8~B>!Okq5Q-7pXMLQKbrqp{^$9} z@>}x1$p13`c>Y&Z_SgB}mUS8>I7Itubg$)!E2eeXcO=QAI z(U^-3DCLvfg1E3hvV?n5heW(oOuZvdM`{SAU!U{Kq4%f}rTEfB^xeKgCN_GI_vp z$*XZ?TFQjHg%AVh89k7NST$;*rqv7!AmRfr9_=!vGN`3)s*@}U83B!gcPs>`jwAWs zQ>tvL0ccn}!juACO)0afDJANmDJ=!*E|9ldqaD2fLmTq?8ySQJ+GOL~&;!*Qdx4_6 z0rD{UQQWxGtXXPuGSCP3A&~uqNCBYJzl@Acfaw#OKY|aDvzDZ%OS*W76z*fRDGXWctYXmKkN^?>5V1YlBpcZI%31~Rj@pp<80KU>mpqvB&la8

BYOoBN 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

    {ti_^P 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

    NC0Lla363g5Nwm z+6zLlDS=%%;5bh}ZXDLSYq_QEImgmt6DfV4U#G<0)!uD^y=(~)ZD(yGJhFY)fdyO# z;&<=%~-m0tGg}-3|+zAmbzawQb$`aBVod zsb|fg?H3UAZBS7X52s_ih-+xZEjyntv%40nT~LEE0(>Sr!FqpUeg{*#Uo$Q@hHJ(myG*_M00MJ)J`fFn-9Q~ zga$1ZwB$`Z(b9PN8RTt#9Y!q`GlgJ1b_UZT2pl)=E7pLfopud?A2I#y#*?a<$KDgf z8_a@!g>4A&EKxCO`AKeU1AFdhZ(urWH7B+v5PJ%57dyONjQ%pJ*R+K;M`f8@+)iac z`7_3JBhclBRm=h;U&s7x#FHve;Uo+bR z3DBD*k_xm;VTCkfkui@nqs&V(2g8X2!^N2!F#pUZLEo!U<%v#1rW^hNi6BrC&rFA70 z`kJ`Ati3~Vst4=Mdp}pm;y`)3G^j^6GsHn%lYxdI!KB;g)ubp!2*ylD=9))>*0_rJ zwqv>m3<8RcjYI23`w!&6bJ2~clfq214=!7qG!oAs-&=YVwa(f7f$IZzcKgXpvkSXZ z>Mu_xJ>#JW#NI&7h%*qOOSna_=PHhi0%z(=sy{_>!*KMN$*_7v4?ssHDggcH3ZF1C z2B2?=30lTy1LeNTlhLH$^^CHUVkl=}BsxuHkn?V}R=-!`k)T>?!l8k9=SB(?7@KXxN&0-x!Gq{CvNw}stA# zWB;38F+)OSP4dt`)&a3Nt58k;ikCb{$#m#7@{^JV1dFkkhbS?i`l1!HiE781f*;@> zA5+jMO(g46Bc0dKAhIg(U4(Y

    P`H5x%J}xAM6-!FWsML~zg|eVga6HZ2-C_-r>q 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≪6%T;RXc@ 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 RBoX|#2pOduN8ZxV{{fY~dD;K~ literal 0 HcmV?d00001 diff --git a/packages/stream_chat_persistence/example/windows/.gitignore b/packages/stream_chat_persistence/example/windows/.gitignore new file mode 100644 index 00000000..d492d0d9 --- /dev/null +++ b/packages/stream_chat_persistence/example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/packages/stream_chat_persistence/example/windows/CMakeLists.txt b/packages/stream_chat_persistence/example/windows/CMakeLists.txt new file mode 100644 index 00000000..1633297a --- /dev/null +++ b/packages/stream_chat_persistence/example/windows/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.14) +project(example LANGUAGES CXX) + +set(BINARY_NAME "example") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Configure build options. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() + +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/packages/stream_chat_persistence/example/windows/flutter/CMakeLists.txt b/packages/stream_chat_persistence/example/windows/flutter/CMakeLists.txt new file mode 100644 index 00000000..b2e4bd8d --- /dev/null +++ b/packages/stream_chat_persistence/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,103 @@ +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/packages/stream_chat_persistence/example/windows/flutter/generated_plugin_registrant.cc b/packages/stream_chat_persistence/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..988f3c8f --- /dev/null +++ b/packages/stream_chat_persistence/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + Sqlite3FlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); +} diff --git a/packages/stream_chat_persistence/example/windows/flutter/generated_plugin_registrant.h b/packages/stream_chat_persistence/example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..dc139d85 --- /dev/null +++ b/packages/stream_chat_persistence/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/stream_chat_persistence/example/windows/flutter/generated_plugins.cmake b/packages/stream_chat_persistence/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 00000000..8abff957 --- /dev/null +++ b/packages/stream_chat_persistence/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + sqlite3_flutter_libs +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/stream_chat_persistence/example/windows/runner/CMakeLists.txt b/packages/stream_chat_persistence/example/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..de2d8916 --- /dev/null +++ b/packages/stream_chat_persistence/example/windows/runner/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) +apply_standard_settings(${BINARY_NAME}) +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/packages/stream_chat_persistence/example/windows/runner/Runner.rc b/packages/stream_chat_persistence/example/windows/runner/Runner.rc new file mode 100644 index 00000000..5fdea291 --- /dev/null +++ b/packages/stream_chat_persistence/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#ifdef FLUTTER_BUILD_NUMBER +#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#else +#define VERSION_AS_NUMBER 1,0,0 +#endif + +#ifdef FLUTTER_BUILD_NAME +#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "example.exe" "\0" + VALUE "ProductName", "example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/packages/stream_chat_persistence/example/windows/runner/flutter_window.cpp b/packages/stream_chat_persistence/example/windows/runner/flutter_window.cpp new file mode 100644 index 00000000..b43b9095 --- /dev/null +++ b/packages/stream_chat_persistence/example/windows/runner/flutter_window.cpp @@ -0,0 +1,61 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/packages/stream_chat_persistence/example/windows/runner/flutter_window.h b/packages/stream_chat_persistence/example/windows/runner/flutter_window.h new file mode 100644 index 00000000..6da0652f --- /dev/null +++ b/packages/stream_chat_persistence/example/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/packages/stream_chat_persistence/example/windows/runner/main.cpp b/packages/stream_chat_persistence/example/windows/runner/main.cpp new file mode 100644 index 00000000..bcb57b0e --- /dev/null +++ b/packages/stream_chat_persistence/example/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/packages/stream_chat_persistence/example/windows/runner/resource.h b/packages/stream_chat_persistence/example/windows/runner/resource.h new file mode 100644 index 00000000..66a65d1e --- /dev/null +++ b/packages/stream_chat_persistence/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/packages/stream_chat_persistence/example/windows/runner/resources/app_icon.ico b/packages/stream_chat_persistence/example/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c04e20caf6370ebb9253ad831cc31de4a9c965f6 GIT binary patch literal 33772 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 literal 0 HcmV?d00001 diff --git a/packages/stream_chat_persistence/example/windows/runner/runner.exe.manifest b/packages/stream_chat_persistence/example/windows/runner/runner.exe.manifest new file mode 100644 index 00000000..c977c4a4 --- /dev/null +++ b/packages/stream_chat_persistence/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/packages/stream_chat_persistence/example/windows/runner/utils.cpp b/packages/stream_chat_persistence/example/windows/runner/utils.cpp new file mode 100644 index 00000000..d19bdbbc --- /dev/null +++ b/packages/stream_chat_persistence/example/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + if (target_length == 0) { + return std::string(); + } + std::string utf8_string; + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/packages/stream_chat_persistence/example/windows/runner/utils.h b/packages/stream_chat_persistence/example/windows/runner/utils.h new file mode 100644 index 00000000..3879d547 --- /dev/null +++ b/packages/stream_chat_persistence/example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/stream_chat_persistence/example/windows/runner/win32_window.cpp b/packages/stream_chat_persistence/example/windows/runner/win32_window.cpp new file mode 100644 index 00000000..c10f08dc --- /dev/null +++ b/packages/stream_chat_persistence/example/windows/runner/win32_window.cpp @@ -0,0 +1,245 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/packages/stream_chat_persistence/example/windows/runner/win32_window.h b/packages/stream_chat_persistence/example/windows/runner/win32_window.h new file mode 100644 index 00000000..17ba4311 --- /dev/null +++ b/packages/stream_chat_persistence/example/windows/runner/win32_window.h @@ -0,0 +1,98 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart index af0dc874..f84fbd6b 100644 --- a/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart +++ b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart @@ -3942,9 +3942,6 @@ class MemberEntity extends DataClass implements Insertable { /// The channel cid of which this user is part of final String channelCid; - /// The role of the user in the channel - final String? role; - /// The role of the user in the channel final String? channelRole; @@ -3974,7 +3971,6 @@ class MemberEntity extends DataClass implements Insertable { MemberEntity( {required this.userId, required this.channelCid, - this.role, this.channelRole, this.inviteAcceptedAt, this.inviteRejectedAt, @@ -3991,8 +3987,6 @@ class MemberEntity extends DataClass implements Insertable { .mapFromDatabaseResponse(data['${effectivePrefix}user_id'])!, channelCid: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}channel_cid'])!, - role: const StringType() - .mapFromDatabaseResponse(data['${effectivePrefix}role']), channelRole: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}channel_role']), inviteAcceptedAt: const DateTimeType().mapFromDatabaseResponse( @@ -4018,9 +4012,6 @@ class MemberEntity extends DataClass implements Insertable { final map = {}; map['user_id'] = Variable(userId); map['channel_cid'] = Variable(channelCid); - if (!nullToAbsent || role != null) { - map['role'] = Variable(role); - } if (!nullToAbsent || channelRole != null) { map['channel_role'] = Variable(channelRole); } @@ -4045,7 +4036,6 @@ class MemberEntity extends DataClass implements Insertable { return MemberEntity( userId: serializer.fromJson(json['userId']), channelCid: serializer.fromJson(json['channelCid']), - role: serializer.fromJson(json['role']), channelRole: serializer.fromJson(json['channelRole']), inviteAcceptedAt: serializer.fromJson(json['inviteAcceptedAt']), @@ -4065,7 +4055,6 @@ class MemberEntity extends DataClass implements Insertable { return { 'userId': serializer.toJson(userId), 'channelCid': serializer.toJson(channelCid), - 'role': serializer.toJson(role), 'channelRole': serializer.toJson(channelRole), 'inviteAcceptedAt': serializer.toJson(inviteAcceptedAt), 'inviteRejectedAt': serializer.toJson(inviteRejectedAt), @@ -4081,7 +4070,6 @@ class MemberEntity extends DataClass implements Insertable { MemberEntity copyWith( {String? userId, String? channelCid, - Value role = const Value.absent(), Value channelRole = const Value.absent(), Value inviteAcceptedAt = const Value.absent(), Value inviteRejectedAt = const Value.absent(), @@ -4094,7 +4082,6 @@ class MemberEntity extends DataClass implements Insertable { MemberEntity( userId: userId ?? this.userId, channelCid: channelCid ?? this.channelCid, - role: role.present ? role.value : this.role, channelRole: channelRole.present ? channelRole.value : this.channelRole, inviteAcceptedAt: inviteAcceptedAt.present ? inviteAcceptedAt.value @@ -4114,7 +4101,6 @@ class MemberEntity extends DataClass implements Insertable { return (StringBuffer('MemberEntity(') ..write('userId: $userId, ') ..write('channelCid: $channelCid, ') - ..write('role: $role, ') ..write('channelRole: $channelRole, ') ..write('inviteAcceptedAt: $inviteAcceptedAt, ') ..write('inviteRejectedAt: $inviteRejectedAt, ') @@ -4132,7 +4118,6 @@ class MemberEntity extends DataClass implements Insertable { int get hashCode => Object.hash( userId, channelCid, - role, channelRole, inviteAcceptedAt, inviteRejectedAt, @@ -4148,7 +4133,6 @@ class MemberEntity extends DataClass implements Insertable { (other is MemberEntity && other.userId == this.userId && other.channelCid == this.channelCid && - other.role == this.role && other.channelRole == this.channelRole && other.inviteAcceptedAt == this.inviteAcceptedAt && other.inviteRejectedAt == this.inviteRejectedAt && @@ -4163,7 +4147,6 @@ class MemberEntity extends DataClass implements Insertable { class MembersCompanion extends UpdateCompanion { final Value userId; final Value channelCid; - final Value role; final Value channelRole; final Value inviteAcceptedAt; final Value inviteRejectedAt; @@ -4176,7 +4159,6 @@ class MembersCompanion extends UpdateCompanion { const MembersCompanion({ this.userId = const Value.absent(), this.channelCid = const Value.absent(), - this.role = const Value.absent(), this.channelRole = const Value.absent(), this.inviteAcceptedAt = const Value.absent(), this.inviteRejectedAt = const Value.absent(), @@ -4190,7 +4172,6 @@ class MembersCompanion extends UpdateCompanion { MembersCompanion.insert({ required String userId, required String channelCid, - this.role = const Value.absent(), this.channelRole = const Value.absent(), this.inviteAcceptedAt = const Value.absent(), this.inviteRejectedAt = const Value.absent(), @@ -4205,7 +4186,6 @@ class MembersCompanion extends UpdateCompanion { static Insertable custom({ Expression? userId, Expression? channelCid, - Expression? role, Expression? channelRole, Expression? inviteAcceptedAt, Expression? inviteRejectedAt, @@ -4219,7 +4199,6 @@ class MembersCompanion extends UpdateCompanion { return RawValuesInsertable({ if (userId != null) 'user_id': userId, if (channelCid != null) 'channel_cid': channelCid, - if (role != null) 'role': role, if (channelRole != null) 'channel_role': channelRole, if (inviteAcceptedAt != null) 'invite_accepted_at': inviteAcceptedAt, if (inviteRejectedAt != null) 'invite_rejected_at': inviteRejectedAt, @@ -4235,7 +4214,6 @@ class MembersCompanion extends UpdateCompanion { MembersCompanion copyWith( {Value? userId, Value? channelCid, - Value? role, Value? channelRole, Value? inviteAcceptedAt, Value? inviteRejectedAt, @@ -4248,7 +4226,6 @@ class MembersCompanion extends UpdateCompanion { return MembersCompanion( userId: userId ?? this.userId, channelCid: channelCid ?? this.channelCid, - role: role ?? this.role, channelRole: channelRole ?? this.channelRole, inviteAcceptedAt: inviteAcceptedAt ?? this.inviteAcceptedAt, inviteRejectedAt: inviteRejectedAt ?? this.inviteRejectedAt, @@ -4270,9 +4247,6 @@ class MembersCompanion extends UpdateCompanion { if (channelCid.present) { map['channel_cid'] = Variable(channelCid.value); } - if (role.present) { - map['role'] = Variable(role.value); - } if (channelRole.present) { map['channel_role'] = Variable(channelRole.value); } @@ -4308,7 +4282,6 @@ class MembersCompanion extends UpdateCompanion { return (StringBuffer('MembersCompanion(') ..write('userId: $userId, ') ..write('channelCid: $channelCid, ') - ..write('role: $role, ') ..write('channelRole: $channelRole, ') ..write('inviteAcceptedAt: $inviteAcceptedAt, ') ..write('inviteRejectedAt: $inviteRejectedAt, ') @@ -4341,11 +4314,6 @@ class $MembersTable extends Members type: const StringType(), requiredDuringInsert: true, $customConstraints: 'REFERENCES channels(cid) ON DELETE CASCADE'); - final VerificationMeta _roleMeta = const VerificationMeta('role'); - @override - late final GeneratedColumn role = GeneratedColumn( - 'role', aliasedName, true, - type: const StringType(), requiredDuringInsert: false); final VerificationMeta _channelRoleMeta = const VerificationMeta('channelRole'); @override @@ -4416,7 +4384,6 @@ class $MembersTable extends Members List get $columns => [ userId, channelCid, - role, channelRole, inviteAcceptedAt, inviteRejectedAt, @@ -4450,10 +4417,6 @@ class $MembersTable extends Members } else if (isInserting) { context.missing(_channelCidMeta); } - if (data.containsKey('role')) { - context.handle( - _roleMeta, role.isAcceptableOrUnknown(data['role']!, _roleMeta)); - } if (data.containsKey('channel_role')) { context.handle( _channelRoleMeta, diff --git a/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart b/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart index ca9a98bf..a30dd59d 100644 --- a/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart +++ b/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart @@ -15,12 +15,11 @@ import 'package:stream_chat_persistence/stream_chat_persistence.dart'; /// specifically for native platform applications. class SharedDB { /// Returns a new instance of [DriftChatDatabase]. - static Future constructDatabase( + static DriftChatDatabase constructDatabase( String userId, { bool logStatements = false, ConnectionMode connectionMode = ConnectionMode.regular, - bool webUseIndexedDbIfSupported = false, // Ignored on native - }) async { + }) { final dbName = 'db_$userId'; if (connectionMode == ConnectionMode.background) { return DriftChatDatabase.connect( diff --git a/packages/stream_chat_persistence/lib/src/db/shared/unsupported_db.dart b/packages/stream_chat_persistence/lib/src/db/shared/unsupported_db.dart index b92105c5..c840aa5f 100644 --- a/packages/stream_chat_persistence/lib/src/db/shared/unsupported_db.dart +++ b/packages/stream_chat_persistence/lib/src/db/shared/unsupported_db.dart @@ -5,11 +5,10 @@ import 'package:stream_chat_persistence/stream_chat_persistence.dart'; /// A Helper class to construct new instances of [DriftChatDatabase] class SharedDB { /// Returns a new instance of [DriftChatDatabase]. - static Future constructDatabase( + static DriftChatDatabase constructDatabase( String userId, { bool logStatements = false, ConnectionMode connectionMode = ConnectionMode.regular, - bool webUseIndexedDbIfSupported = false, }) { throw UnsupportedError( 'No implementation of the constructDatabase api provided', diff --git a/packages/stream_chat_persistence/lib/src/db/shared/web_db.dart b/packages/stream_chat_persistence/lib/src/db/shared/web_db.dart index 89dc864b..394b38d0 100644 --- a/packages/stream_chat_persistence/lib/src/db/shared/web_db.dart +++ b/packages/stream_chat_persistence/lib/src/db/shared/web_db.dart @@ -7,32 +7,13 @@ import 'package:stream_chat_persistence/src/stream_chat_persistence_client.dart' /// specifically for Web applications. class SharedDB { /// Returns a new instance of [DriftChatDatabase]. - static Future constructDatabase( + static DriftChatDatabase constructDatabase( String userId, { bool logStatements = false, ConnectionMode connectionMode = ConnectionMode.regular, // Ignored on web - bool webUseIndexedDbIfSupported = false, - }) async { + }) { final dbName = 'db_$userId'; - final queryExecutor = await _getQueryExecutor( - dbName, - useIndexedDbIfSupported: webUseIndexedDbIfSupported, - logStatements: logStatements, - ); + final queryExecutor = WebDatabase(dbName, logStatements: logStatements); return DriftChatDatabase(userId, queryExecutor); } - - static Future _getQueryExecutor( - String dbName, { - required bool useIndexedDbIfSupported, - required bool logStatements, - }) async { - if (useIndexedDbIfSupported) { - return WebDatabase.withStorage( - await DriftWebStorage.indexedDbIfSupported(dbName), - logStatements: logStatements, - ); - } - return WebDatabase(dbName, logStatements: logStatements); - } } diff --git a/packages/stream_chat_persistence/lib/src/entity/members.dart b/packages/stream_chat_persistence/lib/src/entity/members.dart index 371cb2be..b4e8181b 100644 --- a/packages/stream_chat_persistence/lib/src/entity/members.dart +++ b/packages/stream_chat_persistence/lib/src/entity/members.dart @@ -11,10 +11,6 @@ class Members extends Table { TextColumn get channelCid => text().customConstraint('REFERENCES channels(cid) ON DELETE CASCADE')(); - /// The role of the user in the channel - @Deprecated('Please use channelRole') - TextColumn get role => text().nullable()(); - /// The role of the user in the channel TextColumn get channelRole => text().nullable()(); diff --git a/packages/stream_chat_persistence/lib/src/mapper/member_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/member_mapper.dart index 05975ea4..e1b3bac2 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/member_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/member_mapper.dart @@ -11,7 +11,6 @@ extension MemberEntityX on MemberEntity { shadowBanned: shadowBanned, updatedAt: updatedAt, createdAt: createdAt, - role: role, channelRole: channelRole, inviteAcceptedAt: inviteAcceptedAt, invited: invited, @@ -33,8 +32,6 @@ extension MemberX on Member { inviteRejectedAt: inviteRejectedAt, invited: invited, inviteAcceptedAt: inviteAcceptedAt, - // ignore: deprecated_member_use - role: role, channelRole: channelRole, updatedAt: updatedAt, ); diff --git a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart index 28b93588..6eb29139 100644 --- a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart +++ b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:mutex/mutex.dart'; import 'package:stream_chat/stream_chat.dart'; + import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; /// Various connection modes on which [StreamChatPersistenceClient] can work @@ -28,14 +29,8 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { /// Connection mode on which the client will work ConnectionMode connectionMode = ConnectionMode.regular, Level logLevel = Level.WARNING, - - /// Whether to use an experimental storage implementation on the web - /// that uses IndexedDB if the current browser supports it. - /// Otherwise, falls back to the local storage based implementation. - bool webUseExperimentalIndexedDb = false, LogHandlerFunction? logHandlerFunction, }) : _connectionMode = connectionMode, - _webUseIndexedDbIfSupported = webUseExperimentalIndexedDb, _logger = Logger.detached('💽')..level = logLevel { _logger.onRecord.listen(logHandlerFunction ?? _defaultLogHandler); } @@ -46,7 +41,6 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { final Logger _logger; final ConnectionMode _connectionMode; - final bool _webUseIndexedDbIfSupported; final _mutex = ReadWriteMutex(); void _defaultLogHandler(LogRecord record) { @@ -74,15 +68,11 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { return true; } - Future _defaultDatabaseProvider( + DriftChatDatabase _defaultDatabaseProvider( String userId, ConnectionMode mode, ) => - SharedDB.constructDatabase( - userId, - connectionMode: mode, - webUseIndexedDbIfSupported: _webUseIndexedDbIfSupported, - ); + SharedDB.constructDatabase(userId, connectionMode: mode); @override Future connect( @@ -96,7 +86,7 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { ); } db = databaseProvider?.call(userId, _connectionMode) ?? - await _defaultDatabaseProvider(userId, _connectionMode); + _defaultDatabaseProvider(userId, _connectionMode); } @override diff --git a/packages/stream_chat_persistence/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index a2c9feb4..0afd9484 100644 --- a/packages/stream_chat_persistence/pubspec.yaml +++ b/packages/stream_chat_persistence/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_persistence homepage: https://github.com/GetStream/stream-chat-flutter description: Official Stream Chat Persistence library. Build your own chat experience using Dart and Flutter. -version: 4.4.0 +version: 5.0.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -19,7 +19,7 @@ dependencies: path: ^1.8.0 path_provider: ^2.0.1 sqlite3_flutter_libs: ^0.5.0 - stream_chat: ^4.6.0 + stream_chat: ^5.0.0 dev_dependencies: build_runner: ^2.0.1 dart_code_metrics: ^4.4.0 diff --git a/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart index 1082967e..1d4d151b 100644 --- a/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart @@ -30,7 +30,7 @@ void main() { isModerator: math.Random().nextBool(), invited: math.Random().nextBool(), inviteAcceptedAt: DateTime.now(), - role: 'testRole', + channelRole: 'testRole', updatedAt: DateTime.now(), ), ); @@ -108,7 +108,7 @@ void main() { isModerator: math.Random().nextBool(), invited: math.Random().nextBool(), inviteAcceptedAt: DateTime.now(), - role: 'testRole', + channelRole: 'testRole', updatedAt: DateTime.now(), ); await database.userDao.updateUsers([newUser]); diff --git a/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart index e327e752..206651bd 100644 --- a/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart @@ -15,7 +15,7 @@ void main() { channelCid: 'testCid', createdAt: DateTime.now(), updatedAt: DateTime.now(), - role: 'testRole', + channelRole: 'testRole', inviteAcceptedAt: DateTime.now(), inviteRejectedAt: DateTime.now(), invited: math.Random().nextBool(), @@ -44,7 +44,7 @@ void main() { user: user, createdAt: DateTime.now(), updatedAt: DateTime.now(), - role: 'testRole', + channelRole: 'testRole', inviteAcceptedAt: DateTime.now(), inviteRejectedAt: DateTime.now(), invited: math.Random().nextBool(),

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 YQ(Z;*sqd?4d~qzytV|m&-}&Xg0DR#%Pyhe` literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/message_dialog_0.png b/packages/stream_chat_flutter/test/src/goldens/message_dialog_0.png new file mode 100644 index 0000000000000000000000000000000000000000..1a7faf9cc65575a22a5f8394add54b3a52d757f0 GIT binary patch literal 43765 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 literal 10173 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 literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/sending_indicator_2.png b/packages/stream_chat_flutter/test/src/goldens/sending_indicator_2.png new file mode 100644 index 0000000000000000000000000000000000000000..dc823015dd8de98872ee97baf1b810f898a650e9 GIT binary patch literal 24082 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 literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/stream_chat_context_menu_item_0.png b/packages/stream_chat_flutter/test/src/goldens/stream_chat_context_menu_item_0.png new file mode 100644 index 0000000000000000000000000000000000000000..9b6e8ed06174648a58971e1401c3959fe7273f40 GIT binary patch literal 27654 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 AivR!s literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/upload_progress_indicator_1.png b/packages/stream_chat_flutter/test/src/goldens/upload_progress_indicator_1.png new file mode 100644 index 0000000000000000000000000000000000000000..1d936b36e9c008fa3614f78c3be14c673fcacc92 GIT binary patch literal 28011 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 literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/upload_progress_indicator_2.png b/packages/stream_chat_flutter/test/src/goldens/upload_progress_indicator_2.png new file mode 100644 index 0000000000000000000000000000000000000000..8bb24319dad47b0d21ed76dda81131d1f208792d GIT binary patch literal 28109 zcmeHwc{tQ-*#De5MVnGc)*2@&TcYfgMkR%kCEHkvB3stXSk7s&#yO`fSu075StQw) zWT=E>%dVM3#=Z?>d!O$NW?a9&fA4j@=Nf-#W<1+{-=F()FP^VL4fM|L*eb9U27~R; zIfu9mgKe>f!8kZMH-k^|n`ys;e}410d{zsFs}uSK-u&*NrE`T7e4#n7g}`9@VLFIY zS9}wPy2_k7slwIcc@}rYbxyYgX^FdDeG?bca+ibrZ(xP41ZwQ>cOmyqei4%TaBZ2J zD=qHr>AgGdpS*2;Y}20)WS)J~d!QQ9^sK=6pX+l|U%RONtMZAB$HLYV9R9 zfvxFZ!X64wn%<4L2leJnCM6XX<+aXiH|{m-*tj~VS8X(4?9}2+PW@mz&9B1{ zH$*f+HX}vU9Z{Pl_m^*28TT{?Vd|L#+qUvHNN}$x!+VVrwf{8?;9d zaEukSnJe_Z;kJ<$D~;b{GabxS*D1$i6=wGIbvanpZARS74V!h;yVvZ`)a7R9XzrFH zkZ{I%OqKE%8zD?DRzkv!>|PV@=+>GDLVN^tv@WK;trcRpZF0FG;R??(kM_#qV1=VR zbTqGU=Fu8-;8`+^b$2n>Js%6^^3Yc}#a!Wr^llq3IcSAkededHEO~6aluP3i&YF&P z<{zkVeAj;Kgz8Nc^ zwh!7ALQslgj^_KLf+WS<%Hl81@W$UwSH*YJflK{lzTLIU=F&mtQgNKySZ4mo>D0*_ z>ZT3P8fRL&e0K<;N@q>+FkN_sfGBga)IXd~7R&q`) zQ%&@|^WyaP_g0n_v|3tWKc_5poD5$_2SG9GcmO86e;omM-s8jsvMhZ7o)y5o<}4?xLkP6fOfiK2s2F7Na?i({ zn0c1THt;MLCOw_329CyLAecJXS)3{36xbQXGFhzujH%|*H((d6t4m9;esWWhqAM4? zf(eq7^@$G&Tr)F+{cTypSGN^;FZwNxG^C!gs4#<~gLBj>EMYREYc_XsLKTe+jkeON z&UEDPo$#M~ovi6=Aj`GW$;lyoWMnv3{nu9uCT5dWsLB;4`1a&n!gOtI;pD74Ue<=m zRZpuCro*f!3MI5Ni8JW8T{gKa>e>hxBbn(-n-TY#S$t`G1SdwBX=MKW+{W@ueH=A_ zXEigGSsoNhFlJHaMu`eDVWu)8f;gRyvrOI%&+22D91=o^VVNu@R$<11wurwuos?N7 zUxsI4SSN=PaA%kp5L0~22|1EL6rsgFr%lHWxfXF%DC^X&fu4AQvn=ZdCQT9&ZH=Xl zG>5-`-`?JqQG`NCF2CHi#v><b}L`4HFa#8{uQ7siMI7_DmWv#tyaWM#~jM_q} zTMEFs-g=|%WYZWcOPRaN@zA2;_HV198#WuA6H7?$DML1{S+Gim#d<-lAp}0AxnOk% zEBsWMN*p@_&kASSI%r!cq5A?;@ZxAr3YuvzYv3xaETiv-5&|wVM_Yt(QkI!YEl&$2 zEU}mXfiPt*)w{Jyg!Pz``#kP3c@&1bmBk&ygpp~O+!>b1);Fv8SSP0m7_v@|agka# zIaH2yXY1>AXG{Z^2U1m(HC!OQD=JbJJMu7@bq@(Fhd-YbNeGyo>7Y$?mvmcH&_>#F zXtZCA%ZnO+f3Q@`r;H9`|BH@tMrCo!kb}9m^A?nM|*Q zN&mWhFS^pih^gVH+Lj@Nud|uwnFe~3Rm({^GHw*j+;Sl31b}|kx`|@(5ri0Sq!Ejy zxo*NM%#6`SEGAO12bs+=GUf;qO6~x7!AL zY@JBze5c8}-8iai4-TKA;p};rMIqQmZeu~UQ=F@lrdodWb9OM3XP6(hmXih6Qz zXydea; zt}#Y!I+Pzy<+~4FWa@ z{$n6mdXE#PHygH384G2-x26 zAM=KDYK4g);M4B{{KrINi^Jxv6%4@Gyv6ngwl}c70ce0t1#Bu{QvsU_*i`WUy8#wb zQuGj;lO?LOL}`&%GV(SxKO-?LdgzVzKJQ(duRp!F$?gMBh)rmy{h`wRn{>yJ06l-HC(Di2+Y&XlVzyD9ZZf^C1W-Me?+2sWpQcMz53 z&PsL)T;czo^bFqZlGYEu(g9A|+niYJut1d}ZH&+C@gwuN?w!Ytv zT=lS_wRNw*zkjYsaH^?~kB_I1&sp%LaQt|)pAfWFD&yK)@2-#ilMNjmf?#Z)Fs|3l z(b@Uo$jHdRuV37D&@}`h#!b|q!!cmp>A_|&)|_(bX;yl=M2yr`cqO!HoGty%LD1pL zhj?sjrxE6ZhYxkHV7<=gukE1$Q zgl-6QFZ-w$pu;(yzrN-Vy{>Uohp7#Y-oss#FMcUXuI{cB^8W=3CS@|w&w~Ekr>7+r=;Y2ZY1U7 zHQSk?Hg4{QFJE@;*s&wEW&8H+l^;HQIB-V;fq3)wZB?yek5mO+NBij)kIH}dA9g~9 z?+`q3p>6TllM8R&yg_()czjM)SG`@(nctdaS#;vTg9k5fZRXbn7Vh+HyqGHj>8&@! z07;?W3yJ()(gc_IdNwmH?cmSaguTwr&PK?Nrlvp2Nq=_$;AENQ9sy@Jqg;Xx{!*Q> zuaX}Mm7165^Obtj#U*mnHok~C$5U}GB7OtUZa6uGCnqNZxxx6OScWrk*}AzM1)5dP zK>fH~pfo>}lUh)4C+y{4@9p|NMK`p!3t+KWhMuk4<1aCe_7a2E5S28}ifd9YEG#5_ z)s^K%x>uTI+iKGd2_65a^UC_YrSl`90#lGuufQA_@IdJ|#oN^HbmLkE%%CaN@w|XcUnB z^y$+^$eqEuhK7dd@7s{Crum@_8^eU@^g9DNj~BZ6%a^}-ki1*(&$gB=@owC>(a4e` z_uu+{U>-$3KR-tX!rw3+M)+1e*o?uH+JCRO5AL85ZRM7hsAV0)VPapw=bPyFGv^r| zRV9uq^O(QL;_LyVJs}qpPFx%aNKf)M>vVg+ z}RALf=#BO~58m{zGFHH5W z+_&Zie{xwzRr;d&MYG!AV4al;KJ+b>hh`G zsA>y9@Z`50F5xs^N!isRf?kXN9@MpMe0ee3rK|An!j2|A zBoa9_HPz&|9rB@g#+_xv72yKFdl5x1!3k*76vMMBgXoDzDG~B@N_L+{pioy-R7A>2 zPYF;doyNKn!(~hPxw##Z@7P0p=-%mG9o<^ouRf}59pu$)WMY&U1yC8rD@g(<4Gjqa z;LRVe5+px1)H{s7n(t=Qi5e}rB*iC*>!Q}v5_%TIN6MC$LPC58o&}S1k+jPoDr#3c z;q^N;%T!y1Nk2d?7YcP*8{{!j+Y2 zo;hmu)DfSmeGUkjSjm*VfE$|9HH;$ha5#ttq{)^E zgsRQ<-sLFnY1G#g27J?(^kpXM)y!gE{k3DMIL)~qk0g_GO=M!j59-D|t`A(AOzF_x z0ghoEpR*Bq0?6$%2%+!qwVqH^jM~>UivC!GBFs8um5+CdUX>njD2l9Z&FecZ>yDe z+x0#&kRm!tbK;+<$4HElf&x$3eAg(K%4uxdCg_*fC+Sz94WhFvk`_Uf;1?0m*xo2q zG_#ZN)}>2jR<~Zlu^UV=Yqb)o0Y5rrGKC>g8o*LFvr(Mo~_xg>ZdIaU|i)S$T zRaI3`@UH0@ei+9mW5J)E?B_T~a}AhGAlDf#BiVF0SzUrdhZ?QLEd z0tbKzVTb6V3r%L;dDy=6tj=^Oz-!>RqM}J+*(@oXY&AJiBWsdapQM_HRs&WK8x`Me zT{87mm$!fZtWDqoSu_=o$D@-!UVEOMoh^inKA$%GZfC%*joqlTrL;jvu4~7h0A3#N^r$EL04w?e;wCj2>OTZy%Fwmy)XqJCPWMsgAn`8j zWN^rlZ@+~*s?!Ei#|KiQM8@U?U5m5P#WwFB_>w{;xT@(f{Q@`RY;m?ABWV$HyO$4u zMy}kW9}yC=;zw7>q0p+YVi?KA)6Mqv$-=rZQioG|0+$!%jvd=Ak?5vn5x6+r(9*&O zl4Fpm?&-*LjS3bGPzo3iM>e_HLJL&ca?qcpj8p#{2vK5EBjECvP=B-TCE<0pxP-Gi?slP<$O291H`T^$LRt z2T`OdR3e#w)UUOOG}GZKOMeihe5yB(e#hPDL;oZh_p$GGu+6`A9bg^eT%xi^RmsnX zu4p^r+&0-_1C4GF*@!+skcxb2$MTc;de5WV_R2&w>ZyWS=zQ8`^khO$1V|`m$AUF2 zxQf6hx4H^*;XDbdJ}wrh;oLm|T#$^xjB3X^>^fA3xHWYY-|-;MAvR=zlTM5$tD}y1 zH?(VJq23F2A01f~5EPWGqekU(y(ld$g+Dhd@^)4OJ_NE6lS+W;z1(=BNlmP5ls=lY zXS^XQKtaTRs$qf_E>n0zs~*Y^d&d|_O?7cgOUvOBq=(1kPV@ZGvL8e7%~X#`5=_ zfuf}bH|C1&)nRK-M)5n zzMy^`2UvexPZ=EiR6Sp1j4FY6mJAHNMSawugPI!&ZC=fIn8WQeWoRgIto{4!-2iOZEs}Z(vL^_GNN=s(inAP za3_pjWSwjlf4$8Zg(|WnaAhNxX{~*&RBgoPBvnP1lIfN>0Li%=U`hGXvHP-D7R+yT z7b^q&o#}D~rB-s_(%i%yP?&W3QLyeUR~@1?p{m#1wImn*puGe|y|*Dt5|{rRjb2`) zM)_U;9=JT;Gcj)7JrNT&TBkZ%9H;DIKeqtwnqN;Z7{}s7P|?twOO|^8C5C{Ece|oGIzFW&p(j4N&kyHSk8~B;RYmOM#ZPxo z95mGFcO z-*@aC$65d7bj`)XGJvTeRv<^J%D9v*&Y0%dUj%-=WxUR(G^f~w(t?GOp~(5-)3qZe zka_Wp#~dr2hDU72T3ucSBa_O(^wE#)+a8zxVQ01#f`PwX7evD zE|PII*N$adzuSO^3svJRi;mR!jCNZ#PixM7)F-L-%;^n%%N9GI_Vv{$6|k{v@t78R zCMQMx20FLZE=rv*WpT)Xe1ESMzlQ?`>5A^(mlqh5e=mZTyW6Br}RB5pwKf6cD zTw3~F)kSdiF2Is$S-|48s?@C@QdJxH_U zs3?gP^s(eSKc5_qpDG71^7Qtuf{H_sYmEcPsCs!ZfoskKGaijc9|R>Fwi-IbD`iF# zf)XQ(6uEo%?jEb2=4M`~)-#(2YgU6k*Wit#SWi#SO3*N|pvXa!UT$K9KwnFJeSIU! z{QUe*no5Cx5-3OH0+$vZXJ@v6uFLT7aB$yMp@hPsA}9JI#pNr7v#(|DC7o>3h-%S5 zbOoiOqk{~BG}b#S)6>g~m=W0< z2%z&OQ?@X1bj)0-`XA7X zNo!Fk{AHHfgYr5XBz{})t~YLkY?H~~&nIohQ(%D`>>qY={gMkB7B_C(cr_~%TLZO1 zUX1LV*PYu3MH8j?=HbqQoGX+AbhSLv52Q6hM4So5PN>l_F=E)v52a`?j;pDujnDw8 zKs_DcC+;aGQha3Kr*@!lzoNc(2%nuT=TbEM)+~zN$G|e`f|*9pErj}Q&8@k)xkSMG zsu>P@!-*jEj08v4&Nh__>~3n}d2&JgW;NuAl_25I)2ZDA2)oPE5MZE%QQ_T8TkEla zHe5qP1Gk`}OV(iFpMU-d(xhy+?-ehS2yi&Sje?@$Sx`HDowmDq^JXZp0|d3#sM|xo z1_pK^p~FSkoWVlT4Hg#(I*tE&c7Y55l+tf)shPj5-~F{ISL9DIi`Cwa9zAh9`T*3^ zAmp4Y^!D{t0*|N)g48d8l<}2^-l4%y^9#Kj3V?cpZq{C^J|j}ie$WlkpkIel>Z>RE zYElgj{b323de1Q!$@Y$pbl=yZg8cj;)09^IBH;q1+n|e?Km4`>o>05m6=FOQ@3sqo zIvYJVq0=Q6TSElxl+=ujn_({nkQymbAmcs)EW*_xudLDqNg($2LUntvr^GYRl#0?^${!0moPDe9HE76Ytl%o1fvXM1STo+8>Qi4HcS-`xfQ0*V zf}-mu+bp2$Zy>(`ldhj1;eeVYQkI2pR{Ciu2GqAeO}Sg0`R?aO+rEC?4U&Lx3z@w+ z4)w`nUiY;QJO~c1X)ww*P5fG*^ww*gTSjuOtZ)Pv6tqJ5g@n|u)qm)_4Vn``_=hwX zkTnWfZNVvB0qt7y&jeSE7x+(5o~;zmKwG8(Xsa}58ZlbQ)}N-QrYgUt=@RLrZ^X!) z{(z#x0*KStY5t?vaRoo)p)N9mO_WHCb#{aP74cKFB(PeOAJ4g%T6;Snazj^_C8PBj zd5&>X9dN`5*TDG>$Z6>9WYFWjUK6A3VoW_?{0@c0H1&C^FaVeul3BJW<2gEAY{;#K2zzW zYH?ga0f9h(UkQK~miCDRY@aW5z-l4-0il<$lh60%iqKy{-+IrnJWRWiZ_2r46rf4#sC`w|BDz{O0iU>{{c7tAFk@bT|{oi-mpi^zb12Z3%#*U z{{j1BhJM&OVdDhGc9Inku)Trp4Qy{%!2pcy4Qy{SetY!jhaNw!qHy}1Zdj1c+TUx3 z#r;nIIkW)#;}tcsb;3{t8xU+jfcsr+hM>cM?G0>iV0*&~2L2n#!UnBdS2Ri*Enh%V N>1gXAa9Y-X{STxx=J)^r literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/user_avatar_0.png b/packages/stream_chat_flutter/test/src/goldens/user_avatar_0.png new file mode 100644 index 0000000000000000000000000000000000000000..a155ccc4960e02132050dcb3e22b0e998b2bdd54 GIT binary patch literal 33579 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