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 00000000..eb9b4d76 Binary files /dev/null and b/packages/stream_chat_flutter/example/web/icons/Icon-maskable-192.png differ 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 00000000..d69c5669 Binary files /dev/null and b/packages/stream_chat_flutter/example/web/icons/Icon-maskable-512.png differ 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 00000000..c04e20ca Binary files /dev/null and b/packages/stream_chat_flutter/example/windows/runner/resources/app_icon.ico differ 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 00000000..2294ea84 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/attachment_button_0.png differ 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 00000000..8c4b011c Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/attachment_modal_sheet_0.png differ 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 00000000..a6503af8 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/clear_input_item_0.png differ 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 00000000..72719d49 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/command_button_0.png differ 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 00000000..9a2676ad Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/confirmation_dialog_0.png differ 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 00000000..e18ed82d Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/countdown_button_0.png differ 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 00000000..9e7f6a73 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/delete_message_dialog_0.png differ 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 00000000..39535158 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/dm_checkbox_0.png differ 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 00000000..f07b2a9d Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/dm_checkbox_1.png differ 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 00000000..a5b1eda7 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/dm_checkbox_2.png differ 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 00000000..2b593918 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/download_menu_item_0.png differ diff --git a/packages/stream_chat_flutter/test/src/goldens/edit_message_sheet_0.png b/packages/stream_chat_flutter/test/src/goldens/edit_message_sheet_0.png new file mode 100644 index 00000000..babe1ef3 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/edit_message_sheet_0.png differ 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 00000000..f484ee2a Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/error_alert_sheet_0.png differ diff --git a/packages/stream_chat_flutter/test/src/goldens/gallery_footer_0.png b/packages/stream_chat_flutter/test/src/goldens/gallery_footer_0.png new file mode 100644 index 00000000..8f21a6ca Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/gallery_footer_0.png differ 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 00000000..a0b780e9 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/gallery_header_0.png differ 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 00000000..bb14fc09 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/group_avatar_0.png differ 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 00000000..1a7faf9c Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/message_dialog_0.png differ diff --git a/packages/stream_chat_flutter/test/src/goldens/message_dialog_1.png b/packages/stream_chat_flutter/test/src/goldens/message_dialog_1.png new file mode 100644 index 00000000..ea7f428c Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/message_dialog_1.png differ diff --git a/packages/stream_chat_flutter/test/src/goldens/message_dialog_2.png b/packages/stream_chat_flutter/test/src/goldens/message_dialog_2.png new file mode 100644 index 00000000..75313e05 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/message_dialog_2.png differ diff --git a/packages/stream_chat_flutter/test/src/goldens/message_text.png b/packages/stream_chat_flutter/test/src/goldens/message_text.png index dda158b2..df038fe5 100644 Binary files a/packages/stream_chat_flutter/test/src/goldens/message_text.png and b/packages/stream_chat_flutter/test/src/goldens/message_text.png differ diff --git a/packages/stream_chat_flutter/test/src/goldens/send_button_0.png b/packages/stream_chat_flutter/test/src/goldens/send_button_0.png new file mode 100644 index 00000000..087a0352 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/send_button_0.png differ diff --git a/packages/stream_chat_flutter/test/src/goldens/sending_indicator_0.png b/packages/stream_chat_flutter/test/src/goldens/sending_indicator_0.png new file mode 100644 index 00000000..27eb2714 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/sending_indicator_0.png differ diff --git a/packages/stream_chat_flutter/test/src/goldens/sending_indicator_1.png b/packages/stream_chat_flutter/test/src/goldens/sending_indicator_1.png new file mode 100644 index 00000000..dc823015 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/sending_indicator_1.png differ 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 00000000..dc823015 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/sending_indicator_2.png differ 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 00000000..9b6e8ed0 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/stream_chat_context_menu_item_0.png differ diff --git a/packages/stream_chat_flutter/test/src/goldens/upload_progress_indicator_0.png b/packages/stream_chat_flutter/test/src/goldens/upload_progress_indicator_0.png new file mode 100644 index 00000000..9b5f1769 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/upload_progress_indicator_0.png differ 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 00000000..1d936b36 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/upload_progress_indicator_1.png differ 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 00000000..8bb24319 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/upload_progress_indicator_2.png differ 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 00000000..a155ccc4 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/user_avatar_0.png differ 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 00000000..189e3432 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/user_avatar_1.png differ diff --git a/packages/stream_chat_flutter/test/src/image_footer_test.dart b/packages/stream_chat_flutter/test/src/image_footer_test.dart deleted file mode 100644 index 2e2771b4..00000000 --- a/packages/stream_chat_flutter/test/src/image_footer_test.dart +++ /dev/null @@ -1,52 +0,0 @@ -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( - 'it should show channel typing', - (WidgetTester tester) async { - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel(); - final 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', - }); - - 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 00000000..3c4935a7 Binary files /dev/null and b/packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ 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 00000000..ed4cc164 Binary files /dev/null and b/packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 00000000..483be613 Binary files /dev/null and b/packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ 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 00000000..bcbf36df Binary files /dev/null and b/packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 00000000..9c0a6528 Binary files /dev/null and b/packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ 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 00000000..e71a7261 Binary files /dev/null and b/packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 00000000..8a31fe2d Binary files /dev/null and b/packages/stream_chat_persistence/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/packages/stream_chat_persistence/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/stream_chat_persistence/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 00000000..80e867a4 --- /dev/null +++ b/packages/stream_chat_persistence/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 00000000..8aaa46ac Binary files /dev/null and b/packages/stream_chat_persistence/example/web/favicon.png differ 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 00000000..b749bfef Binary files /dev/null and b/packages/stream_chat_persistence/example/web/icons/Icon-192.png differ 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 00000000..88cfd48d Binary files /dev/null and b/packages/stream_chat_persistence/example/web/icons/Icon-512.png differ 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 00000000..eb9b4d76 Binary files /dev/null and b/packages/stream_chat_persistence/example/web/icons/Icon-maskable-192.png differ 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 00000000..d69c5669 Binary files /dev/null and b/packages/stream_chat_persistence/example/web/icons/Icon-maskable-512.png differ 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(!(0:_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 00000000..c04e20ca Binary files /dev/null and b/packages/stream_chat_persistence/example/windows/runner/resources/app_icon.ico differ 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(),