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/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/melos.yaml b/melos.yaml index 10e7ca7b..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/** @@ -54,11 +51,11 @@ scripts: description: Build all generated files for Dart & Flutter packages in this project. generate:dart: - run: melos exec -c 1 --depends-on="build_runner" --no-flutter -- "dart run build_runner build --delete-conflicting-outputs" + run: melos exec -c 1 --depends-on="build_runner" --no-flutter -- "dart run build_runner build --delete-conflicting-outputs --enable-experiment=super-parameters,enhanced-enums" description: Build all generated files for Dart packages in this project. generate:flutter: - run: melos exec -c 1 --depends-on="build_runner" --flutter -- "flutter pub run build_runner build --delete-conflicting-outputs" + run: melos exec -c 1 --depends-on="build_runner" --flutter -- "flutter run build_runner build --delete-conflicting-outputs --enable-experiment=super-parameters,enhanced-enums" description: Build all generated files for Flutter packages in this project. test:all: @@ -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 2d17f32e..37e45f9e 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,76 @@ +## 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 + +- Added `StreamChatClient.getCallToken` and `StreamChatClient.createCall` methods. + +🐞 Fixed + +- Only listen to client events when the user is connected to the websocket. + +## 4.5.0 + +🐞 Fixed + +- Fix `Channel.removeMessage` not able to remove thread message. + +✅ Added + +- Added `hide_history` flag in `client.addChannelMembers`, `channel.addMembers`. + +## 4.4.1 + +🐞 Fixed + +- Do not serialize `AttachmentFile.bytes` + +## 4.4.0 + +🐞 Fixed + +- Fix WebSocket contemporary connection calls while disconnecting + +✅ Added + +- Export `StreamAttachmentFileUploader`. + +🔄 Changed + +- Deprecated `StreamChatClient.attachmentFileUploader`, + Use `StreamChatClient.attachmentFileUploaderProvider` instead. + ## 4.3.0 🐞 Fixed diff --git a/packages/stream_chat/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 b4e1379d..d2d854a8 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -3,12 +3,15 @@ import 'dart:math'; import 'package:collection/collection.dart' show IterableExtension, ListEquality; -import 'package:dio/dio.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/client/retry_queue.dart'; import 'package:stream_chat/src/core/util/utils.dart'; import 'package:stream_chat/stream_chat.dart'; +/// The maximum time the incoming [Event.typingStart] event is valid before a +/// [Event.typingStop] event is emitted automatically. +const incomingTypingStartEventTimeout = 7; + /// Class that manages a specific channel. /// /// #### Channel name @@ -1061,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!, @@ -1146,27 +1149,34 @@ class Channel { /// Add members to the channel. Future addMembers( - List memberIds, [ + List memberIds, { Message? message, - ]) async { + bool hideHistory = false, + }) async { _checkInitialized(); - return _client.addChannelMembers(id!, type, memberIds, message: message); + return _client.addChannelMembers( + id!, + type, + memberIds, + message: message, + hideHistory: hideHistory, + ); } /// Invite members to the channel. 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); } @@ -1494,36 +1504,38 @@ class Channel { ) .where((e) => e.cid == cid); - DateTime? _lastTypingEvent; + late final _keyStrokeHandler = KeyStrokeHandler( + onStartTyping: startTyping, + onStopTyping: stopTyping, + ); - /// First of the [EventType.typingStart] and [EventType.typingStop] events - /// based on the users keystrokes. Call this on every keystroke. + /// Sends the [Event.typingStart] event and schedules a timer to invoke the + /// [Event.typingStop] event. + /// + /// This is meant to be called every time the user presses a key. Future keyStroke([String? parentId]) async { - if (config?.typingEvents == false) { - return; - } + if (config?.typingEvents == false) return; - client.logger.info('start typing'); - final now = DateTime.now(); - - if (_lastTypingEvent == null || - now.difference(_lastTypingEvent!).inSeconds >= 2) { - _lastTypingEvent = now; - await sendEvent(Event( - type: EventType.typingStart, - parentId: parentId, - )); - } + client.logger.info('KeyStroke received'); + return _keyStrokeHandler(parentId); } - /// Sets last typing to null and sends the typing.stop event. + /// Sends the [EventType.typingStart] event. + Future startTyping([String? parentId]) async { + if (config?.typingEvents == false) return; + + client.logger.info('start typing'); + await sendEvent(Event( + type: EventType.typingStart, + parentId: parentId, + )); + } + + /// Sends the [EventType.typingStop] event. Future stopTyping([String? parentId]) async { - if (config?.typingEvents == false) { - return; - } + if (config?.typingEvents == false) return; client.logger.info('stop typing'); - _lastTypingEvent = null; await sendEvent(Event( type: EventType.typingStop, parentId: parentId, @@ -1533,6 +1545,7 @@ class Channel { /// Call this method to dispose the channel client. void dispose() { state?.dispose(); + _keyStrokeHandler.cancel(); } void _checkInitialized() { @@ -1594,9 +1607,9 @@ class ChannelClientState { _listenMemberUnbanned(); - _startCleaning(); + _startCleaningStaleTypingEvents(); - _startCleaningPinnedMessages(); + _startCleaningStalePinnedMessages(); _channel._client.chatPersistenceClient ?.getChannelThreads(_channel.cid!) @@ -1614,7 +1627,8 @@ class ChannelClientState { }); } - final _subscriptions = []; + final Channel _channel; + final _subscriptions = CompositeSubscription(); void _checkExpiredAttachmentMessages(ChannelState channelState) async { final expiredAttachmentMessagesId = channelState.messages @@ -1787,20 +1801,15 @@ class ChannelClientState { /// Retry failed message. Future retryFailedMessages() async { - final failedMessages = - [...messages, ...threads.values.expand((v) => v)] - .where( - (message) => - message.status != MessageSendingStatus.sent && - message.createdAt.isBefore( - DateTime.now().subtract( - const Duration( - seconds: 5, - ), - ), - ), - ) - .toList(); + final failedMessages = [...messages, ...threads.values.expand((v) => v)] + .where( + (message) => + message.status != MessageSendingStatus.sent && + message.createdAt.isBefore( + DateTime.now().subtract(const Duration(seconds: 5)), + ), + ) + .toList(); _retryQueue.add(failedMessages); } @@ -1956,11 +1965,13 @@ class ChannelClientState { // Early return in case the thread is not available if (!newThreads.containsKey(parentId)) return; - _threads = newThreads - ..update( - parentId, - (messages) => messages..removeWhere((e) => e.id == message.id), - ); + // Remove thread message shown in thread page. + newThreads.update( + parentId, + (messages) => [...messages.where((e) => e.id != message.id)], + ); + + _threads = newThreads; // Early return if the thread message is not shown in channel. if (message.showInChannel == false) return; @@ -1985,12 +1996,7 @@ class ChannelClientState { } _subscriptions.add( - _channel - .on( - EventType.messageRead, - EventType.notificationMarkRead, - ) - .listen( + _channel.on(EventType.messageRead, EventType.notificationMarkRead).listen( (event) { final readList = List.from(_channelState.read ?? []); final userReadIndex = @@ -2078,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; @@ -2251,34 +2253,29 @@ class ChannelClientState { ); } - /// Channel related typing users last value. - Map get typingEvents => _typingEventsController.value; - /// Channel related typing users stream. Stream> get typingEventsStream => _typingEventsController.stream; - final BehaviorSubject> _typingEventsController = - BehaviorSubject.seeded({}); - - final Channel _channel; - final Map _typings = {}; + /// Channel related typing users last value. + Map get typingEvents => _typingEventsController.value; + final _typingEventsController = BehaviorSubject.seeded({}); void _listenTypingEvents() { - if (_channelState.channel?.config.typingEvents == false) { - return; - } + if (_channelState.channel?.config.typingEvents == false) return; + + final currentUser = _channel.client.state.currentUser; + if (currentUser == null) return; _subscriptions ..add( _channel.on(EventType.typingStart).listen( (event) { - if (event.user != null) { - final user = event.user!; - if (user.id != _channel.client.state.currentUser?.id) { - _typings[user] = event; - _typingEventsController.add(_typings); - } + final user = event.user; + if (user != null && user.id != currentUser.id) { + final events = {...typingEvents}; + events[user] = event; + _typingEventsController.add(events); } }, ), @@ -2286,112 +2283,109 @@ class ChannelClientState { ..add( _channel.on(EventType.typingStop).listen( (event) { - if (event.user != null) { - final user = event.user!; - if (user.id != _channel.client.state.currentUser?.id) { - _typings.remove(event.user); - _typingEventsController.add(_typings); - } + final user = event.user; + if (user != null && user.id != currentUser.id) { + final events = {...typingEvents}..remove(user); + _typingEventsController.add(events); } }, ), ) ..add( - _channel - .on() - .where((event) => - event.user != null && - members.any((m) => m.userId == event.user!.id)) - .listen( + _channel.on().where((event) { + final user = event.user; + if (user == null) return false; + return members.any((m) => m.userId == user.id); + }).listen( (event) { final newMembers = List.from(members); final oldMemberIndex = newMembers.indexWhere((m) => m.userId == event.user!.id); if (oldMemberIndex > -1) { final oldMember = newMembers.removeAt(oldMemberIndex); - updateChannelState(ChannelState( - members: [ - ...newMembers, - oldMember.copyWith( - user: event.user, - ), - ], - )); + updateChannelState( + ChannelState( + members: [ + ...newMembers, + oldMember.copyWith( + user: event.user, + ), + ], + ), + ); } }, ), ); } - Timer? _cleaningTimer; + Timer? _staleTypingEventsCleanerTimer; - void _startCleaning() { - if (_channelState.channel?.config.typingEvents == false) { - return; - } + // Checks and removes stale typing events that were not explicitly stopped by + // the sender due to technical difficulties. e.g. process death, loss of + // Internet connection or custom implementation. + void _startCleaningStaleTypingEvents() { + if (_channelState.channel?.config.typingEvents == false) return; - _cleaningTimer = Timer.periodic(const Duration(seconds: 1), (_) { - final now = DateTime.now(); - - if (_channel._lastTypingEvent != null && - now.difference(_channel._lastTypingEvent!).inSeconds > 1) { - _channel.stopTyping(); - } - - _clean(); - }); + _staleTypingEventsCleanerTimer = Timer.periodic( + const Duration(seconds: 1), + (_) { + final now = DateTime.now(); + typingEvents.forEach((user, event) { + if (now.difference(event.createdAt).inSeconds > + incomingTypingStartEventTimeout) { + _channel.client.handleEvent( + Event( + type: EventType.typingStop, + user: user, + cid: _channel.cid, + parentId: event.parentId, + ), + ); + } + }); + }, + ); } - late Timer _pinnedMessagesTimer; + Timer? _stalePinnedMessagesCleanerTimer; - void _startCleaningPinnedMessages() { - _pinnedMessagesTimer = Timer.periodic(const Duration(seconds: 30), (_) { - final now = DateTime.now(); - var expiredMessages = channelState.pinnedMessages - ?.where((m) => m.pinExpires?.isBefore(now) == true) - .toList(); - if (expiredMessages != null && expiredMessages.isNotEmpty) { - expiredMessages = expiredMessages - .map((m) => m.copyWith( - pinExpires: null, - pinned: false, - )) + // Checks and removes stale pinned messages that are not valid anymore. + void _startCleaningStalePinnedMessages() { + _stalePinnedMessagesCleanerTimer = Timer.periodic( + const Duration(seconds: 30), + (_) { + final now = DateTime.now(); + var expiredMessages = channelState.pinnedMessages + ?.where((m) => m.pinExpires?.isBefore(now) == true) .toList(); + if (expiredMessages != null && expiredMessages.isNotEmpty) { + expiredMessages = expiredMessages + .map((m) => m.copyWith( + pinExpires: null, + pinned: false, + )) + .toList(); - updateChannelState(_channelState.copyWith( - pinnedMessages: pinnedMessages.where(_pinIsValid).toList(), - messages: expiredMessages, - )); - } - }); - } - - void _clean() { - final now = DateTime.now(); - _typings.forEach((user, event) { - if (now.difference(event.createdAt).inSeconds > 7) { - _channel.client.handleEvent( - Event( - type: EventType.typingStop, - user: user, - cid: _channel.cid, - parentId: event.parentId, - ), - ); - } - }); + updateChannelState(_channelState.copyWith( + pinnedMessages: pinnedMessages.where(_pinIsValid).toList(), + messages: expiredMessages, + )); + } + }, + ); } /// Call this method to dispose this object. void dispose() { _debouncedUpdatePersistenceChannelState.cancel(); _retryQueue.dispose(); - _subscriptions.forEach((s) => s.cancel()); + _subscriptions.cancel(); _channelStateController.close(); _isUpToDateController.close(); _threadsController.close(); - _cleaningTimer?.cancel(); - _pinnedMessagesTimer.cancel(); + _staleTypingEventsCleanerTimer?.cancel(); + _stalePinnedMessagesCleanerTimer?.cancel(); _typingEventsController.close(); } } diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 0b31db9e..2a33bd35 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -70,7 +70,8 @@ class StreamChatClient { Duration receiveTimeout = const Duration(seconds: 6), StreamChatApi? chatApi, WebSocket? ws, - AttachmentFileUploader? attachmentFileUploader, + AttachmentFileUploaderProvider attachmentFileUploaderProvider = + StreamAttachmentFileUploader.new, }) { logger.info('Initiating new StreamChatClient'); @@ -87,7 +88,7 @@ class StreamChatClient { options: options, tokenManager: _tokenManager, connectionIdManager: _connectionIdManager, - attachmentFileUploader: attachmentFileUploader, + attachmentFileUploaderProvider: attachmentFileUploaderProvider, logger: detachedLogger('🕸️'), ); @@ -380,6 +381,10 @@ class StreamChatClient { user, includeUserDetails: includeUserDetailsInConnectCall, ); + + // Start listening to events + state.subscribeToEvents(); + return user.merge(event.me); } catch (e, stk) { logger.severe('error connecting ws', e, stk); @@ -401,6 +406,9 @@ class StreamChatClient { _connectionStatusSubscription?.cancel(); _connectionStatusSubscription = null; + // Stop listening to events + state.cancelEventSubscription(); + _ws.disconnect(); } @@ -568,6 +576,25 @@ class StreamChatClient { } } + /// Returns a token associated with the [callId]. + Future getCallToken(String callId) async => + _chatApi.call.getCallToken(callId); + + /// Creates a new call. + Future createCall({ + required String callId, + required String callType, + required String channelType, + required String channelId, + }) { + return _chatApi.call.createCall( + callId: callId, + callType: callType, + channelType: channelType, + channelId: channelId, + ); + } + /// Requests channels with a given query from the API. Future> queryChannelsOnline({ Filter? filter, @@ -1027,12 +1054,14 @@ class StreamChatClient { String channelType, List memberIds, { Message? message, + bool hideHistory = false, }) => _chatApi.channel.addMembers( channelId, channelType, memberIds, message: message, + hideHistory: hideHistory, ); /// Remove members from the channel @@ -1452,29 +1481,40 @@ class StreamChatClient { /// The class that handles the state of the channel listening to the events class ClientState { /// Creates a new instance listening to events and updating the state - ClientState(this._client) { - _subscriptions.addAll([ - _client + ClientState(this._client); + + CompositeSubscription? _eventsSubscription; + + /// Starts listening to the client events. + void subscribeToEvents() { + if (_eventsSubscription != null) { + cancelEventSubscription(); + } + + _eventsSubscription = CompositeSubscription(); + _eventsSubscription! + ..add(_client .on() .where((event) => event.me != null && event.type != EventType.healthCheck) .map((e) => e.me!) - .listen((user) => currentUser = currentUser?.merge(user) ?? user), - _client + .listen((user) { + currentUser = currentUser?.merge(user) ?? user; + })) + ..add(_client .on() .map((event) => event.unreadChannels) .whereType() .listen((count) { currentUser = currentUser?.copyWith(unreadChannels: count); - }), - _client + })) + ..add(_client .on() .map((event) => event.totalUnreadCount) .whereType() .listen((count) { currentUser = currentUser?.copyWith(totalUnreadCount: count); - }), - ]); + })); _listenChannelDeleted(); @@ -1485,56 +1525,73 @@ class ClientState { _listenAllChannelsRead(); } - final _subscriptions = []; + /// Stops listening to the client events. + void cancelEventSubscription() { + if (_eventsSubscription != null) { + _eventsSubscription!.cancel(); + _eventsSubscription = null; + } + } - /// Used internally for optimistic update of unread count - set totalUnreadCount(int unreadCount) { - _totalUnreadCountController.add(unreadCount); + /// Pauses listening to the client events. + void pauseEventSubscription([Future? resumeSignal]) { + _eventsSubscription?.pause(resumeSignal); + } + + /// Resumes listening to the client events. + void resumeEventSubscription() { + _eventsSubscription?.resume(); } void _listenChannelHidden() { - _subscriptions - .add(_client.on(EventType.channelHidden).listen((event) async { - final eventChannel = event.channel!; - await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]); - channels[eventChannel.cid]?.dispose(); - channels = channels..remove(eventChannel.cid); - })); + _eventsSubscription?.add( + _client.on(EventType.channelHidden).listen((event) async { + final eventChannel = event.channel!; + await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]); + channels[eventChannel.cid]?.dispose(); + channels = channels..remove(eventChannel.cid); + }), + ); } void _listenUserUpdated() { - _subscriptions.add(_client.on(EventType.userUpdated).listen((event) { - if (event.user!.id == currentUser!.id) { - currentUser = OwnUser.fromJson(event.user!.toJson()); - } - updateUser(event.user); - })); + _eventsSubscription?.add( + _client.on(EventType.userUpdated).listen((event) { + if (event.user!.id == currentUser!.id) { + currentUser = OwnUser.fromJson(event.user!.toJson()); + } + updateUser(event.user); + }), + ); } void _listenAllChannelsRead() { - _subscriptions - .add(_client.on(EventType.notificationMarkRead).listen((event) { - if (event.cid == null) { - channels.forEach((key, value) { - value.state?.unreadCount = 0; - }); - } - })); + _eventsSubscription?.add( + _client.on(EventType.notificationMarkRead).listen((event) { + if (event.cid == null) { + channels.forEach((key, value) { + value.state?.unreadCount = 0; + }); + } + }), + ); } void _listenChannelDeleted() { - _subscriptions.add(_client - .on( - EventType.channelDeleted, - EventType.notificationRemovedFromChannel, - EventType.notificationChannelDeleted, - ) - .listen((Event event) async { - final eventChannel = event.channel!; - await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]); - channels[eventChannel.cid]?.dispose(); - channels = channels..remove(eventChannel.cid); - })); + _eventsSubscription?.add( + _client + .on( + EventType.channelDeleted, + EventType.notificationRemovedFromChannel, + EventType.notificationChannelDeleted, + ) + .listen((Event event) async { + final eventChannel = event.channel!; + await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]); + channels[eventChannel.cid]?.dispose(); + channels = channels..remove(eventChannel.cid); + }), + ); } final StreamChatClient _client; @@ -1596,6 +1653,11 @@ class ClientState { _channelsController.add(newChannels); } + /// Used internally for optimistic update of unread count + set totalUnreadCount(int unreadCount) { + _totalUnreadCountController.add(unreadCount); + } + void _computeUnreadCounts(OwnUser? user) { final totalUnreadCount = user?.totalUnreadCount; if (totalUnreadCount != null) { @@ -1616,7 +1678,7 @@ class ClientState { /// Call this method to dispose this object void dispose() { - _subscriptions.forEach((s) => s.cancel()); + cancelEventSubscription(); _currentUserController.close(); _unreadChannelsController.close(); _totalUnreadCountController.close(); diff --git a/packages/stream_chat/lib/src/client/key_stroke_handler.dart b/packages/stream_chat/lib/src/client/key_stroke_handler.dart new file mode 100644 index 00000000..52878182 --- /dev/null +++ b/packages/stream_chat/lib/src/client/key_stroke_handler.dart @@ -0,0 +1,115 @@ +import 'dart:async'; + +/// A class that manages buffering typing events and call [onTypingStarted] and +/// [onTypingStopped] accordingly in a timed manner. +/// +/// This class is used by [Channel] to manage typing events. +class KeyStrokeHandler { + /// Creates a new instance of [KeyStrokeHandler]. + KeyStrokeHandler({ + this.startTypingEventTimeout = 1, + this.startTypingResendInterval = 3, + required this.onStartTyping, + required this.onStopTyping, + }); + + /// The number of seconds from the last [onStartTyping] callback until + /// the [onStopTyping] callback is automatically invoked. + final int startTypingEventTimeout; + + /// The number of seconds after the last [onStartTyping] callback before + /// the [onStartTyping] callback is automatically invoked again. + final int startTypingResendInterval; + + /// Called when a `typingStart` event needs to be send. + final Future Function([String? parentId]) onStartTyping; + + /// Called when a `typingStop` event needs to be send. + final Future Function([String? parentId]) onStopTyping; + + Timer? _keyStrokeTimer; + String? _currentParentId; + DateTime? _lastTypingEvent; + Completer? _keyStrokeCompleter; + + Future _startTyping(String? parentId) { + _currentParentId = parentId; + _lastTypingEvent = DateTime.now(); + return onStartTyping(parentId); + } + + Future _stopTyping(String? parentId) { + _currentParentId = null; + _lastTypingEvent = null; + return onStopTyping(parentId); + } + + // Completes the key stroke completer if it is not yet completed. + void _completeKeyStrokeCompleterIfRequired() { + final completer = _keyStrokeCompleter; + if (completer != null && !completer.isCompleted) completer.complete(); + } + + // Completes the completer if available and not yet completed then creates a + // new completer and returns it. + Completer _resetKeyStrokeCompleter() { + _completeKeyStrokeCompleterIfRequired(); + return _keyStrokeCompleter = Completer(); + } + + // Cancels the key stroke timer if it is running. + void _cancelKeyStrokeTimer() { + _keyStrokeTimer?.cancel(); + _keyStrokeTimer = null; + } + + /// Cancels the handler and stops the typing event. + void cancel() { + // If the user is typing, stop typing. + // This is needed to prevent the user from being stuck in typing mode. + if (_lastTypingEvent != null) { + // We don't need to handle the error here + // ignore: no-empty-block + _stopTyping(_currentParentId).catchError((_) {}); + } + _cancelKeyStrokeTimer(); + _completeKeyStrokeCompleterIfRequired(); + } + + /// Invokes the [onStartTyping] callback and schedules a timer to invoke the + /// [onStopTyping] callback. + /// + /// This is meant to be called every time the user presses a key. The method + /// will manage requests and timer as needed. + Future call([String? parentId]) async { + final completer = _resetKeyStrokeCompleter(); + + _cancelKeyStrokeTimer(); + + _keyStrokeTimer = Timer(Duration(seconds: startTypingEventTimeout), () { + _stopTyping(parentId).then((_) { + if (completer.isCompleted) return; + completer.complete(); + }).onError((error, stackTrace) { + if (completer.isCompleted) return; + completer.completeError(error!, stackTrace); + }); + }); + + // If the user is typing too long, it should call [onStartTyping] again. + final now = DateTime.now(); + final lastTypingEvent = _lastTypingEvent; + if (lastTypingEvent == null || + now.difference(lastTypingEvent).inMilliseconds > + // startTypingResendInterval in milliseconds + startTypingResendInterval * 1000) { + _startTyping(parentId).onError((error, stackTrace) { + _cancelKeyStrokeTimer(); + if (completer.isCompleted) return; + completer.completeError(error!, stackTrace); + }); + } + + return completer.future; + } +} diff --git a/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart b/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart index 096b2c2d..c851d114 100644 --- a/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart +++ b/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart @@ -3,6 +3,11 @@ import 'package:stream_chat/src/core/api/responses.dart'; import 'package:stream_chat/src/core/http/stream_http_client.dart'; import 'package:stream_chat/src/core/models/attachment_file.dart'; +/// Signature for a function which provides instance of [AttachmentFileUploader] +typedef AttachmentFileUploaderProvider = AttachmentFileUploader Function( + StreamHttpClient httpClient, +); + /// Class responsible for uploading images and files to a given channel abstract class AttachmentFileUploader { /// Uploads a [image] to the given channel. diff --git a/packages/stream_chat/lib/src/core/api/call_api.dart b/packages/stream_chat/lib/src/core/api/call_api.dart new file mode 100644 index 00000000..2e40fc63 --- /dev/null +++ b/packages/stream_chat/lib/src/core/api/call_api.dart @@ -0,0 +1,41 @@ +import 'package:stream_chat/src/core/api/responses.dart'; +import 'package:stream_chat/src/core/http/stream_http_client.dart'; + +/// Defines the api dedicated to call operations. +class CallApi { + /// Initialize a new call api + CallApi(this._client); + + final StreamHttpClient _client; + + /// Returns a token dedicated to the [callId] + Future getCallToken(String callId) async { + final response = await _client.post( + '/calls/$callId', + data: {}, + ); + // return response.data; + return CallTokenPayload.fromJson(response.data); + } + + /// Creates a new call + Future createCall({ + required String callId, + required String callType, + required String channelType, + required String channelId, + }) async { + final response = await _client.post( + _getChannelUrl(channelId, channelType), + data: { + 'id': callId, + 'type': callType, + }, + ); + // return response.data; + return CreateCallPayload.fromJson(response.data); + } + + String _getChannelUrl(String channelId, String channelType) => + '/channels/$channelType/$channelId/call'; +} diff --git a/packages/stream_chat/lib/src/core/api/channel_api.dart b/packages/stream_chat/lib/src/core/api/channel_api.dart index 93d17870..08f6b6e4 100644 --- a/packages/stream_chat/lib/src/core/api/channel_api.dart +++ b/packages/stream_chat/lib/src/core/api/channel_api.dart @@ -210,12 +210,14 @@ class ChannelApi { String channelType, List memberIds, { Message? message, + bool hideHistory = false, }) async { final response = await _client.post( _getChannelUrl(channelId, channelType), data: { 'add_members': memberIds, 'message': message, + 'hide_history': hideHistory, }, ); return AddMembersResponse.fromJson(response.data); diff --git a/packages/stream_chat/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/responses.dart b/packages/stream_chat/lib/src/core/api/responses.dart index d608c5aa..54b885f4 100644 --- a/packages/stream_chat/lib/src/core/api/responses.dart +++ b/packages/stream_chat/lib/src/core/api/responses.dart @@ -1,7 +1,9 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/client/client.dart'; +import 'package:stream_chat/src/core/api/call_api.dart'; import 'package:stream_chat/src/core/error/error.dart'; import 'package:stream_chat/src/core/models/banned_user.dart'; +import 'package:stream_chat/src/core/models/call_payload.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; import 'package:stream_chat/src/core/models/channel_state.dart'; import 'package:stream_chat/src/core/models/device.dart'; @@ -495,3 +497,31 @@ class OGAttachmentResponse extends _BaseResponse { static OGAttachmentResponse fromJson(Map json) => _$OGAttachmentResponseFromJson(json); } + +/// The response to [CallApi.getCallToken] +@JsonSerializable(createToJson: false) +class CallTokenPayload extends _BaseResponse { + /// Create a new instance from a [json]. + static CallTokenPayload fromJson(Map json) => + _$CallTokenPayloadFromJson(json); + + /// The token to use for the call. + String? token; + + /// The user id specific to Agora. + int? agoraUid; + + /// The appId specific to Agora. + String? agoraAppId; +} + +/// The response to [CallApi.createCall] +@JsonSerializable(createToJson: false) +class CreateCallPayload extends _BaseResponse { + /// Create a new instance from a [json]. + static CreateCallPayload fromJson(Map json) => + _$CreateCallPayloadFromJson(json); + + /// The call object. + CallPayload? call; +} diff --git a/packages/stream_chat/lib/src/core/api/responses.g.dart b/packages/stream_chat/lib/src/core/api/responses.g.dart index 991b39f6..24f5c459 100644 --- a/packages/stream_chat/lib/src/core/api/responses.g.dart +++ b/packages/stream_chat/lib/src/core/api/responses.g.dart @@ -297,3 +297,17 @@ OGAttachmentResponse _$OGAttachmentResponseFromJson( ..title = json['title'] as String? ..titleLink = json['title_link'] as String? ..type = json['type'] as String?; + +CallTokenPayload _$CallTokenPayloadFromJson(Map json) => + CallTokenPayload() + ..duration = json['duration'] as String? + ..token = json['token'] as String? + ..agoraUid = json['agora_uid'] as int? + ..agoraAppId = json['agora_app_id'] as String?; + +CreateCallPayload _$CreateCallPayloadFromJson(Map json) => + CreateCallPayload() + ..duration = json['duration'] as String? + ..call = json['call'] == null + ? null + : CallPayload.fromJson(json['call'] as Map); diff --git a/packages/stream_chat/lib/src/core/api/stream_chat_api.dart b/packages/stream_chat/lib/src/core/api/stream_chat_api.dart index bcf041c1..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 @@ -1,5 +1,6 @@ import 'package:logging/logging.dart'; import 'package:stream_chat/src/core/api/attachment_file_uploader.dart'; +import 'package:stream_chat/src/core/api/call_api.dart'; import 'package:stream_chat/src/core/api/channel_api.dart'; import 'package:stream_chat/src/core/api/device_api.dart'; import 'package:stream_chat/src/core/api/general_api.dart'; @@ -22,9 +23,10 @@ class StreamChatApi { StreamHttpClientOptions? options, TokenManager? tokenManager, ConnectionIdManager? connectionIdManager, - AttachmentFileUploader? attachmentFileUploader, + AttachmentFileUploaderProvider attachmentFileUploaderProvider = + StreamAttachmentFileUploader.new, Logger? logger, - }) : _fileUploader = attachmentFileUploader, + }) : _fileUploaderProvider = attachmentFileUploaderProvider, _client = client ?? StreamHttpClient( apiKey, @@ -35,6 +37,7 @@ class StreamChatApi { ); final StreamHttpClient _client; + final AttachmentFileUploaderProvider _fileUploaderProvider; UserApi? _user; @@ -51,6 +54,11 @@ class StreamChatApi { /// Api dedicated to message operations MessageApi get message => _message ??= MessageApi(_client); + CallApi? _call; + + /// Api dedicated to call operations + CallApi get call => _call ??= CallApi(_client); + ChannelApi? _channel; /// Api dedicated to channel operations @@ -75,5 +83,5 @@ class StreamChatApi { /// Class responsible for uploading images and files to a given channel AttachmentFileUploader get fileUploader => - _fileUploader ??= StreamAttachmentFileUploader(_client); + _fileUploader ??= _fileUploaderProvider.call(_client); } diff --git a/packages/stream_chat/lib/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 1684e005..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,11 +47,12 @@ 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. - @JsonKey(toJson: _toString, fromJson: _fromString) + @JsonKey(ignore: true) final Uint8List? bytes; /// The file size in bytes. @@ -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. @@ -124,13 +141,3 @@ class UploadState with _$UploadState { /// Returns true if state is [Failed] bool get isFailed => this is Failed; } - -Uint8List? _fromString(String? bytes) { - if (bytes == null) return null; - return Uint8List.fromList(bytes.codeUnits); -} - -String? _toString(Uint8List? bytes) { - if (bytes == null) return null; - return String.fromCharCodes(bytes); -} diff --git a/packages/stream_chat/lib/src/core/models/attachment_file.freezed.dart b/packages/stream_chat/lib/src/core/models/attachment_file.freezed.dart index 2ea50924..f0ccc584 100644 --- a/packages/stream_chat/lib/src/core/models/attachment_file.freezed.dart +++ b/packages/stream_chat/lib/src/core/models/attachment_file.freezed.dart @@ -224,7 +224,9 @@ class _$Preparing extends Preparing { @override Map toJson() { - return _$$PreparingToJson(this); + return _$$PreparingToJson( + this, + ); } } @@ -392,7 +394,9 @@ class _$InProgress extends InProgress { @override Map toJson() { - return _$$InProgressToJson(this); + return _$$InProgressToJson( + this, + ); } } @@ -404,8 +408,8 @@ abstract class InProgress extends UploadState { factory InProgress.fromJson(Map json) = _$InProgress.fromJson; - int get uploaded => throw _privateConstructorUsedError; - int get total => throw _privateConstructorUsedError; + int get uploaded; + int get total; @JsonKey(ignore: true) _$$InProgressCopyWith<_$InProgress> get copyWith => throw _privateConstructorUsedError; @@ -531,7 +535,9 @@ class _$Success extends Success { @override Map toJson() { - return _$$SuccessToJson(this); + return _$$SuccessToJson( + this, + ); } } @@ -686,7 +692,9 @@ class _$Failed extends Failed { @override Map toJson() { - return _$$FailedToJson(this); + return _$$FailedToJson( + this, + ); } } @@ -696,7 +704,7 @@ abstract class Failed extends UploadState { factory Failed.fromJson(Map json) = _$Failed.fromJson; - String get error => throw _privateConstructorUsedError; + String get error; @JsonKey(ignore: true) _$$FailedCopyWith<_$Failed> get copyWith => throw _privateConstructorUsedError; diff --git a/packages/stream_chat/lib/src/core/models/attachment_file.g.dart b/packages/stream_chat/lib/src/core/models/attachment_file.g.dart index 6b657c2a..fe02433e 100644 --- a/packages/stream_chat/lib/src/core/models/attachment_file.g.dart +++ b/packages/stream_chat/lib/src/core/models/attachment_file.g.dart @@ -11,14 +11,12 @@ AttachmentFile _$AttachmentFileFromJson(Map json) => size: json['size'] as int?, path: json['path'] as String?, name: json['name'] as String?, - bytes: _fromString(json['bytes'] as String?), ); Map _$AttachmentFileToJson(AttachmentFile instance) => { 'path': instance.path, 'name': instance.name, - 'bytes': _toString(instance.bytes), 'size': instance.size, }; diff --git a/packages/stream_chat/lib/src/core/models/call_payload.dart b/packages/stream_chat/lib/src/core/models/call_payload.dart new file mode 100644 index 00000000..e11ae788 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/call_payload.dart @@ -0,0 +1,72 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'call_payload.g.dart'; + +/// Model containing the information about a call. +@JsonSerializable(createToJson: false) +class CallPayload extends Equatable { + /// Create a new instance. + const CallPayload({ + required this.id, + required this.provider, + this.agora, + this.hms, + }); + + /// Create a new instance from a [json]. + factory CallPayload.fromJson(Map json) => + _$CallPayloadFromJson(json); + + /// The call id. + final String id; + + /// The call provider. + final String provider; + + /// The payload specific to Agora. + final AgoraPayload? agora; + + /// The payload specific to 100ms. + final HMSPayload? hms; + + @override + List get props => [id, provider, agora, hms]; +} + +/// Payload for Agora call. +@JsonSerializable(createToJson: false) +class AgoraPayload extends Equatable { + /// Create a new instance. + const AgoraPayload({required this.channel}); + + /// Create a new instance from a [json]. + factory AgoraPayload.fromJson(Map json) => + _$AgoraPayloadFromJson(json); + + /// The Agora channel. + final String channel; + + @override + List get props => [channel]; +} + +/// Payload for 100ms call. +@JsonSerializable(createToJson: false) +class HMSPayload extends Equatable { + /// Create a new instance. + const HMSPayload({required this.roomId, required this.roomName}); + + /// Create a new instance from a [json]. + factory HMSPayload.fromJson(Map json) => + _$HMSPayloadFromJson(json); + + /// The id of the 100ms room. + final String roomId; + + /// The name of the 100ms room. + final String roomName; + + @override + List get props => [roomId, roomName]; +} diff --git a/packages/stream_chat/lib/src/core/models/call_payload.g.dart b/packages/stream_chat/lib/src/core/models/call_payload.g.dart new file mode 100644 index 00000000..bc786cc5 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/call_payload.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'call_payload.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CallPayload _$CallPayloadFromJson(Map json) => CallPayload( + id: json['id'] as String, + provider: json['provider'] as String, + agora: json['agora'] == null + ? null + : AgoraPayload.fromJson(json['agora'] as Map), + hms: json['hms'] == null + ? null + : HMSPayload.fromJson(json['hms'] as Map), + ); + +AgoraPayload _$AgoraPayloadFromJson(Map json) => AgoraPayload( + channel: json['channel'] as String, + ); + +HMSPayload _$HMSPayloadFromJson(Map json) => HMSPayload( + roomId: json['room_id'] as String, + roomName: json['room_name'] as String, + ); diff --git a/packages/stream_chat/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/src/ws/websocket.dart b/packages/stream_chat/lib/src/ws/websocket.dart index 2287481e..a86ba6c5 100644 --- a/packages/stream_chat/lib/src/ws/websocket.dart +++ b/packages/stream_chat/lib/src/ws/websocket.dart @@ -436,6 +436,9 @@ class WebSocket with TimerHelper { /// Disconnects the WS and releases eventual resources void disconnect() { if (connectionStatus == ConnectionStatus.disconnected) return; + + _resetRequestFlags(resetAttempts: true); + _connectionStatus = ConnectionStatus.disconnected; _logger?.info('Disconnecting web-socket connection'); @@ -447,6 +450,7 @@ class WebSocket with TimerHelper { _stopMonitoringEvents(); _manuallyClosed = true; + _closeWebSocketChannel(); } } diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 0479a378..00b32399 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -1,6 +1,7 @@ library stream_chat; export 'package:async/async.dart'; +export 'package:dio/src/cancel_token.dart'; export 'package:dio/src/dio_error.dart'; export 'package:dio/src/multipart_file.dart'; export 'package:dio/src/options.dart'; @@ -9,8 +10,7 @@ export 'package:logging/logging.dart' show Logger, Level, LogRecord; export 'package:rate_limiter/rate_limiter.dart'; export 'package:uuid/uuid.dart'; -export './src/core/api/attachment_file_uploader.dart' - show AttachmentFileUploader; +export './src/core/api/attachment_file_uploader.dart'; export './src/core/api/requests.dart'; export './src/core/api/requests.dart'; export './src/core/api/responses.dart'; @@ -40,11 +40,12 @@ export './src/permission_type.dart'; export './src/ws/connection_status.dart'; export 'src/client/channel.dart'; export 'src/client/client.dart'; +export 'src/client/key_stroke_handler.dart'; export 'src/core/api/attachment_file_uploader.dart' show AttachmentFileUploader; export 'src/core/api/requests.dart'; export 'src/core/api/requests.dart'; export 'src/core/api/responses.dart'; -export 'src/core/api/stream_chat_api.dart' show PushProvider; +export 'src/core/api/stream_chat_api.dart'; export 'src/core/error/error.dart'; export 'src/core/models/action.dart'; export 'src/core/models/attachment.dart'; @@ -63,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 faafb093..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.3.0'; +const PACKAGE_VERSION = '5.0.0'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index 7c5968cc..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.3.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 f90430f2..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); @@ -2658,16 +2660,22 @@ void main() { }); test( - '''should send `typingStart` event if there is not already a typingEvent or the difference between the two is >= 2 seconds''', + '''should send `typingStart` event if there is not already a typingEvent or the difference between the two is > 3 seconds''', () async { - final typingEvent = Event(type: EventType.typingStart); + final startTypingEvent = Event(type: EventType.typingStart); + final stopTypingEvent = Event(type: EventType.typingStop); when(() => channel.config?.typingEvents).thenReturn(true); when(() => client.sendEvent( channelId, channelType, - any(that: isSameEventAs(typingEvent)), + any(that: isSameEventAs(startTypingEvent)), + )).thenAnswer((_) async => EmptyResponse()); + when(() => client.sendEvent( + channelId, + channelType, + any(that: isSameEventAs(stopTypingEvent)), )).thenAnswer((_) async => EmptyResponse()); await channel.keyStroke(); @@ -2675,7 +2683,12 @@ void main() { verify(() => client.sendEvent( channelId, channelType, - any(that: isSameEventAs(typingEvent)), + any(that: isSameEventAs(startTypingEvent)), + )).called(1); + verify(() => client.sendEvent( + channelId, + channelType, + any(that: isSameEventAs(stopTypingEvent)), )).called(1); }, ); @@ -2688,7 +2701,7 @@ void main() { final typingStopEvent = Event(type: EventType.typingStop); - await channel.keyStroke(); + await channel.stopTyping(); verifyNever(() => client.sendEvent( channelId, diff --git a/packages/stream_chat/test/src/client/key_stroke_handler_test.dart b/packages/stream_chat/test/src/client/key_stroke_handler_test.dart new file mode 100644 index 00000000..d0a13352 --- /dev/null +++ b/packages/stream_chat/test/src/client/key_stroke_handler_test.dart @@ -0,0 +1,73 @@ +// ignore_for_file: avoid_redundant_argument_values + +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/test.dart'; + +mixin OnKeyStrokeEvent { + Future call([String? parentId]); +} + +class OnStartTyping extends Mock implements OnKeyStrokeEvent {} + +class OnStopTyping extends Mock implements OnKeyStrokeEvent {} + +void main() { + final onStartTyping = OnStartTyping(); + final onStopTyping = OnStopTyping(); + late KeyStrokeHandler keyStrokeHandler; + + const startTypingEventTimeout = 1; + const startTypingResendInterval = 2; + + setUp(() { + when(() => onStartTyping(any())).thenAnswer((_) => Future.value()); + when(() => onStopTyping(any())).thenAnswer((_) => Future.value()); + + keyStrokeHandler = KeyStrokeHandler( + onStartTyping: onStartTyping, + onStopTyping: onStopTyping, + startTypingEventTimeout: startTypingEventTimeout, + startTypingResendInterval: startTypingResendInterval, + ); + }); + + tearDown(() { + keyStrokeHandler.cancel(); + clearInteractions(onStartTyping); + clearInteractions(onStopTyping); + }); + + group('call', () { + test('should work fine', () { + expect(keyStrokeHandler.call(), completes); + }); + + test('should call onStartTyping', () async { + keyStrokeHandler.call(); + verify(() => onStartTyping(any())).called(1); + }); + + test('should call onStopTyping', () async { + keyStrokeHandler + ..call() + ..cancel(); + verify(() => onStopTyping(any())).called(1); + }); + + test('should call onStartTyping after startTypingResendInterval', () async { + final watch = Stopwatch()..start(); + while (watch.elapsed.inSeconds <= startTypingResendInterval) { + keyStrokeHandler.call(); + } + watch.stop(); + verify(() => onStartTyping(any())).called(2); + }); + + test('should call onStopTyping after startTypingEventTimeout', () async { + keyStrokeHandler.call(); + await Future.delayed(const Duration(seconds: startTypingEventTimeout)); + verify(() => onStopTyping(any())).called(1); + }); + }); +} diff --git a/packages/stream_chat/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/api/call_api_test.dart b/packages/stream_chat/test/src/core/api/call_api_test.dart new file mode 100644 index 00000000..f9e2265f --- /dev/null +++ b/packages/stream_chat/test/src/core/api/call_api_test.dart @@ -0,0 +1,66 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/api/call_api.dart'; +import 'package:test/test.dart'; + +import '../../mocks.dart'; + +void main() { + Response successResponse(String path, {Object? data}) => Response( + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); + + late final client = MockHttpClient(); + late CallApi callApi; + + setUp(() { + callApi = CallApi(client); + }); + + test('getCallToken should work', () async { + const callId = 'test-call-id'; + const path = '/calls/$callId'; + + when(() => client.post(path, data: {})).thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await callApi.getCallToken(callId); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('createCall should work', () async { + const callId = 'test-call-id'; + const callType = 'test-call-type'; + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + const path = '/channels/$channelType/$channelId/call'; + + when(() => client.post( + path, + data: { + 'id': callId, + 'type': callType, + }, + )) + .thenAnswer( + (_) async => successResponse(path, data: {})); + + final res = await callApi.createCall( + callId: callId, + callType: callType, + channelType: channelType, + channelId: channelId, + ); + + expect(res, isNotNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); +} diff --git a/packages/stream_chat/test/src/core/api/channel_api_test.dart b/packages/stream_chat/test/src/core/api/channel_api_test.dart index bd0403de..4e2e4fc2 100644 --- a/packages/stream_chat/test/src/core/api/channel_api_test.dart +++ b/packages/stream_chat/test/src/core/api/channel_api_test.dart @@ -376,6 +376,7 @@ void main() { const memberIds = ['test-member-id-1', 'test-member-id-2']; final channelModel = ChannelModel(id: channelId, type: channelType); final message = Message(id: 'test-message-id', text: 'members-added'); + const hideHistory = true; final path = _getChannelUrl(channelId, channelType); @@ -384,6 +385,7 @@ void main() { data: { 'add_members': memberIds, 'message': message, + 'hide_history': hideHistory, }, )).thenAnswer((_) async => successResponse(path, data: { 'channel': channelModel.toJson(), @@ -395,6 +397,7 @@ void main() { channelType, memberIds, message: message, + hideHistory: hideHistory, ); expect(res, isNotNull); diff --git a/packages/stream_chat/test/src/core/api/requests_test.dart b/packages/stream_chat/test/src/core/api/requests_test.dart index 876a3f61..0bfdca7e 100644 --- a/packages/stream_chat/test/src/core/api/requests_test.dart +++ b/packages/stream_chat/test/src/core/api/requests_test.dart @@ -26,6 +26,56 @@ void main() { } }, ); + + test('copyWith', () { + final params = PaginationParams( + offset: 10, + limit: 20, + createdAtAfter: DateTime.now(), + createdAtAfterOrEqual: DateTime.now(), + createdAtAround: DateTime.now(), + createdAtBefore: DateTime.now(), + createdAtBeforeOrEqual: DateTime.now(), + greaterThan: 'greater-than', + greaterThanOrEqual: 'greater-than-or-equal', + lessThan: 'less-than', + lessThanOrEqual: 'less-than-or-equal', + idAround: 'id-around', + ); + + final sameOld = params.copyWith(); + expect(sameOld, equals(params)); + + final newDateTime = DateTime.now().add(const Duration(days: 2)); + const newTestString = 'test'; + final newParams = params.copyWith( + limit: 2, + offset: 2, + createdAtAfter: newDateTime, + createdAtAfterOrEqual: newDateTime, + createdAtAround: newDateTime, + createdAtBefore: newDateTime, + createdAtBeforeOrEqual: newDateTime, + greaterThan: newTestString, + greaterThanOrEqual: newTestString, + lessThan: newTestString, + lessThanOrEqual: newTestString, + idAround: newTestString, + ); + + expect(newParams.limit, 2); + expect(newParams.offset, 2); + expect(newParams.createdAtAfter, newDateTime); + expect(newParams.createdAtAfterOrEqual, newDateTime); + expect(newParams.createdAtAround, newDateTime); + expect(newParams.createdAtBefore, newDateTime); + expect(newParams.createdAtBeforeOrEqual, newDateTime); + expect(newParams.greaterThan, newTestString); + expect(newParams.greaterThanOrEqual, newTestString); + expect(newParams.lessThan, newTestString); + expect(newParams.lessThanOrEqual, newTestString); + expect(newParams.idAround, newTestString); + }); }); }); } diff --git a/packages/stream_chat/test/src/core/api/responses_test.dart b/packages/stream_chat/test/src/core/api/responses_test.dart index 8a73fccd..073d2b40 100644 --- a/packages/stream_chat/test/src/core/api/responses_test.dart +++ b/packages/stream_chat/test/src/core/api/responses_test.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:stream_chat/src/core/models/call_payload.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; @@ -4340,5 +4341,31 @@ void main() { expect(response.members, isA>()); expect(response.message, isA()); }); + + test('CallTokenPayload', () { + const jsonExample = ''' + {"duration": "3ms", + "agora_app_id":"test", + "agora_uid": 12, + "token": "token"} + '''; + final response = CallTokenPayload.fromJson(json.decode(jsonExample)); + expect(response.agoraAppId, isA()); + expect(response.agoraUid, isA()); + expect(response.token, isA()); + }); + + test('CreateCallPayload', () { + const jsonExample = ''' + {"call": + {"id":"test", + "provider": "test", + "agora": {"channel":"test"}, + "hms":{"room_id":"test", "room_name":"test"} + }} + '''; + final response = CreateCallPayload.fromJson(json.decode(jsonExample)); + expect(response.call, isA()); + }); }); } 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/test/src/core/models/call_payload_test.dart b/packages/stream_chat/test/src/core/models/call_payload_test.dart new file mode 100644 index 00000000..a67e3bee --- /dev/null +++ b/packages/stream_chat/test/src/core/models/call_payload_test.dart @@ -0,0 +1,38 @@ +import 'dart:convert'; + +import 'package:stream_chat/src/core/models/call_payload.dart'; +import 'package:test/test.dart'; + +void main() { + test('CallPayload', () { + const jsonExample = ''' + {"id":"test", + "provider": "test", + "agora": {"channel":"test"}, + "hms":{"room_id":"test", "room_name":"test"} + } + '''; + final response = CallPayload.fromJson(json.decode(jsonExample)); + expect(response.agora, isA()); + expect(response.hms, isA()); + expect(response.id, isA()); + expect(response.provider, isA()); + }); + + test('AgoraPayload', () { + const jsonExample = ''' + {"channel":"test"} + '''; + final response = AgoraPayload.fromJson(json.decode(jsonExample)); + expect(response.channel, isA()); + }); + + test('HMSPayload', () { + const jsonExample = ''' + {"room_id":"test", "room_name":"test"} + '''; + final response = HMSPayload.fromJson(json.decode(jsonExample)); + expect(response.roomId, isA()); + expect(response.roomName, isA()); + }); +} diff --git a/packages/stream_chat/test/src/fakes.dart b/packages/stream_chat/test/src/fakes.dart index 409418cd..ddbf4887 100644 --- a/packages/stream_chat/test/src/fakes.dart +++ b/packages/stream_chat/test/src/fakes.dart @@ -8,7 +8,6 @@ import 'package:stream_chat/src/core/api/general_api.dart'; import 'package:stream_chat/src/core/api/guest_api.dart'; import 'package:stream_chat/src/core/api/message_api.dart'; import 'package:stream_chat/src/core/api/moderation_api.dart'; -import 'package:stream_chat/src/core/api/stream_chat_api.dart'; import 'package:stream_chat/src/core/api/user_api.dart'; import 'package:stream_chat/src/core/http/token.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; diff --git a/packages/stream_chat/test/src/ws/websocket_test.dart b/packages/stream_chat/test/src/ws/websocket_test.dart index 68242333..9489bc78 100644 --- a/packages/stream_chat/test/src/ws/websocket_test.dart +++ b/packages/stream_chat/test/src/ws/websocket_test.dart @@ -131,6 +131,40 @@ void main() { addTearDown(timer.cancel); }); + test('`connect`, `disconnect` and `connect` again without waiting', () async { + final user = OwnUser( + id: 'test-user', + name: 'test', + ); + const connectionId = 'test-connection-id'; + // Sends connect event to web-socket stream + final timer = Timer.periodic(const Duration(milliseconds: 300), (_) { + final event = Event( + type: EventType.healthCheck, + connectionId: connectionId, + me: user, + ); + webSocketSink.add(json.encode(event)); + }); + + await webSocket.connect( + user, + ); + + webSocket + ..disconnect() + ..connect(user) + ..disconnect(); + final event = await webSocket.connect(user); + + expect(event.type, EventType.healthCheck); + expect(event.connectionId, connectionId); + expect(event.me, isNotNull); + expect(event.me!.id, user.id); + + addTearDown(timer.cancel); + }); + test('`connect` should throw if already in connection attempt', () async { final user = OwnUser(id: 'test-user'); webSocket.connect(user); diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index adf61a98..27187060 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,190 @@ +## 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). + +## 4.5.0 + +- Updated `stream_chat_flutter_core` dependency + to [`4.5.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog). + +🐞 Fixed + +- [[#882]](https://github.com/GetStream/stream-chat-flutter/issues/882) Lots of unhandled exceptions + when network is off or spotty. +- Fixes an error where Stream CDN images were not being resized in the message list view. + +🚀 Improved + +- Automatically resize images that are above a specific pixel count to ensure resizing works: + getstream.io/chat/docs/go-golang/file_uploads/#image-resizing + +✅ Added + +- Added `thumbnailSize`, `thumbnailResizeType`, and `thumbnailCropType` params + to `StreamMessageWidget` to customize the appearance of image attachment thumbnails. + + ```dart + StreamMessageListView( + messageBuilder: (context, details, messages, defaultMessage) { + return defaultMessage.copyWith( + imageAttachmentThumbnailSize: ..., + imageAttachmentThumbnailCropType: ..., + imageAttachmentThumbnailResizeType: ..., + ); + }, + ), + ``` + +- Added `thumbnailSize`, `thumbnailFormat`, `thumbnailQuality` and `thumbnailScale` params + to `StreamAttachmentPicker` to customize the appearance of image attachment thumbnails. + + ```dart + StreamMessageInput( + focusNode: _focusNode, + messageInputController: _messageInputController, + attachmentsPickerBuilder: (_, __, picker) { + return picker.copyWith( + attachmentThumbnailSize: ..., + attachmentThumbnailFormat: ..., + attachmentThumbnailQuality: ..., + attachmentThumbnailScale: ..., + ); + }, + ), + ``` + +## 4.4.1 + +🐞 Fixed + +- [[#1247]](https://github.com/GetStream/stream-chat-flutter/issues/1247) Fix Jiffy initialization. +- [[#1232]](https://github.com/getstream/stream-chat-flutter/issues/1232) Fix DateDivider not + showing up in the chat. +- [[#1240]](https://github.com/getstream/stream-chat-flutter/issues/1240) Substitute mentioned user + ids with user names in system message. +- [[#1228]](https://github.com/GetStream/stream-chat-flutter/issues/1228) Fix image download on iOS. + +🔄 Changed + +- Changed default maximum attachment size from 20MB to 100MB. + +## 4.4.0 + +🐞 Fixed + +- [[#1234]](https://github.com/GetStream/stream-chat-flutter/issues/1234) Fix `ChannelListTile` + sendingIndicator `isMessageRead` calculation. + ## 4.3.0 - Updated `photo_view` dependency to [`0.14.0`](https://pub.dev/packages/photo_view/changelog). @@ -9,7 +196,8 @@ - [[#996]](https://github.com/GetStream/stream-chat-flutter/issues/996) Videos break bottom photo carousal. - Fix: URLs with path and/or query params are not enriched. -- [[#1194]](https://github.com/GetStream/stream-chat-flutter/issues/1194) Request permission to access gallery when opening the file picker. +- [[#1194]](https://github.com/GetStream/stream-chat-flutter/issues/1194) Request permission to + access gallery when opening the file picker. ✅ Added diff --git a/packages/stream_chat_flutter/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. --> + + + CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj index 2550e7a5..9b47e613 100644 --- a/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -339,7 +339,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -425,7 +425,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -474,7 +474,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/packages/stream_chat_flutter/example/lib/main.dart b/packages/stream_chat_flutter/example/lib/main.dart index 8864858f..0beffcc0 100644 --- a/packages/stream_chat_flutter/example/lib/main.dart +++ b/packages/stream_chat_flutter/example/lib/main.dart @@ -1,15 +1,18 @@ +// ignore_for_file: public_member_api_docs + import 'package:flutter/material.dart'; +import 'package:responsive_builder/responsive_builder.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_localizations/stream_chat_localizations.dart'; -void main() async { +Future 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 3d0c3314..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,25 +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, + }); - 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, @@ -109,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: () { @@ -123,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', @@ -185,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), @@ -224,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), @@ -240,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( @@ -250,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 c022ee48..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,178 +1,179 @@ 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/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 = '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; + + /// Resize type of the image attachment thumbnail. + /// + /// Defaults to [crop] + final String /*clip|crop|scale|fill*/ imageThumbnailResizeType; + + /// Crop type of the image attachment thumbnail. + /// + /// Defaults to [center] + final String /*center|top|bottom|left|right*/ imageThumbnailCropType; @override - Widget build(BuildContext context) => source.when( - local: () { - 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); - } - - var imageUri = Uri.parse(imageUrl); - if (imageUri.host.endsWith('stream-io-cdn.com') && - imageUri.queryParameters['h'] == '*' && - imageUri.queryParameters['w'] == '*' && - imageUri.queryParameters['crop'] == '*' && - imageUri.queryParameters['resize'] == '*') { - imageUri = imageUri.replace(queryParameters: { - ...imageUri.queryParameters, - 'h': '400', - 'w': '400', - 'crop': 'center', - 'resize': 'crop', - }); - } else if (imageUri.host.endsWith('stream-cloud-uploads.imgix.net')) { - imageUri = imageUri.replace(queryParameters: { - ...imageUri.queryParameters, - 'height': '400', - 'width': '400', - 'fit': 'crop', - }); - } - imageUrl = imageUri.toString(); - - return _buildImageAttachment( - context, - CachedNetworkImage( - cacheKey: imageUri.replace(queryParameters: {}).toString(), - height: size?.height, - width: size?.width, - 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, - ); - }, - imageUrl: imageUrl, - errorWidget: (context, url, error) => AttachmentError(size: size), - fit: BoxFit.cover, - ), - ); - }, - ); - - 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 a3d60483..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']); - 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 cb54c464..00000000 --- a/packages/stream_chat_flutter/lib/src/full_screen_media.dart +++ /dev/null @@ -1,348 +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: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 - with SingleTickerProviderStateMixin { - late final AnimationController _animationController; - late final PageController _pageController; - - late final _curvedAnimation = CurvedAnimation( - parent: _animationController, - curve: Curves.easeOut, - reverseCurve: Curves.easeIn, - ); - - final _opacityTween = Tween(begin: 1, end: 0); - late final _opacityAnimation = _opacityTween.animate( - CurvedAnimation( - parent: _animationController, - curve: const Interval(0, 0.6, curve: Curves.easeOut), - ), - ); - - late final ValueNotifier _currentPage = ValueNotifier(widget.startIndex); - - final videoPackages = {}; - - @override - void initState() { - super.initState(); - _animationController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 300), - ); - _pageController = PageController(initialPage: widget.startIndex); - for (var i = 0; i < widget.mediaAttachmentPackages.length; i++) { - final attachment = widget.mediaAttachmentPackages[i].attachment; - 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: Stack( - children: [ - PageView.builder( - controller: _pageController, - 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 AnimatedBuilder( - animation: _curvedAnimation, - builder: (context, child) => PhotoView( - loadingBuilder: (context, image) => const Offstage(), - imageProvider: (imageUrl == null && - attachment.localUri != null && - attachment.file?.bytes != null) - ? Image.memory(attachment.file!.bytes!).image - : CachedNetworkImageProvider(imageUrl!), - maxScale: PhotoViewComputedScale.covered, - minScale: PhotoViewComputedScale.contained, - heroAttributes: PhotoViewHeroAttributes( - tag: widget.mediaAttachmentPackages, - ), - backgroundDecoration: BoxDecoration( - color: ColorTween( - begin: StreamChannelHeaderTheme.of(context).color, - end: Colors.black, - ).lerp(_curvedAnimation.value), - ), - onTapUp: (a, b, c) { - if (_animationController.isCompleted) { - _animationController.reverse(); - } else { - _animationController.forward(); - } - }, - ), - ); - } else if (attachment.type == 'video') { - final controller = videoPackages[attachment.id]!; - if (!controller.initialized) { - return const Center( - child: CircularProgressIndicator(), - ); - } - return InkWell( - onTap: () { - if (_animationController.isCompleted) { - _animationController.reverse(); - } else { - _animationController.forward(); - } - }, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 50, - ), - child: Chewie( - controller: controller.chewieController!, - ), - ), - ); - } - return const SizedBox(); - }, - itemCount: widget.mediaAttachmentPackages.length, - ), - FadeTransition( - opacity: _opacityAnimation, - child: ValueListenableBuilder( - valueListenable: _currentPage, - builder: (context, value, child) { - final _currentAttachmentPackage = - widget.mediaAttachmentPackages[value]; - final _currentMessage = _currentAttachmentPackage.message; - final _currentAttachment = - _currentAttachmentPackage.attachment; - return Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - StreamGalleryHeader( - userName: widget.userName, - sentAt: context.translations.sentAtText( - date: widget - .mediaAttachmentPackages[_currentPage.value] - .message - .createdAt, - time: widget - .mediaAttachmentPackages[_currentPage.value] - .message - .createdAt, - ), - onBackPressed: () { - Navigator.of(context).pop(); - }, - message: _currentMessage, - attachment: _currentAttachment, - onShowMessage: () { - widget.onShowMessage?.call( - _currentMessage, - StreamChannel.of(context).channel, - ); - }, - attachmentActionsModalBuilder: - widget.attachmentActionsModalBuilder, - ), - if (!_currentMessage.isEphemeral) - StreamGalleryFooter( - currentPage: value, - totalPages: widget.mediaAttachmentPackages.length, - mediaAttachmentPackages: - widget.mediaAttachmentPackages, - mediaSelectedCallBack: (val) { - _currentPage.value = val; - _pageController.animateToPage( - val, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - Navigator.pop(context); - }, - ), - ], - ); - }, - ), - ), - ], - ), - ); - - @override - void dispose() { - _animationController.dispose(); - _pageController.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 cb8f6edd..00000000 --- a/packages/stream_chat_flutter/lib/src/image_group.dart +++ /dev/null @@ -1,157 +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, - }); - - /// 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 iamges - final Size size; - - /// Callback for when show message is tapped - final ShowMessageCallback? onShowMessage; - - @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), - ); -} 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 34b512c4..00000000 --- a/packages/stream_chat_flutter/lib/src/media_list_view.dart +++ /dev/null @@ -1,258 +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, - }); - - /// 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; - - @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( - fadeInDuration: const Duration(milliseconds: 300), - placeholder: const AssetImage( - 'images/placeholder.png', - package: 'stream_chat_flutter', - ), - image: MediaThumbnailProvider( - media: media, - ), - 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 -class MediaThumbnailProvider extends ImageProvider { - /// Constructor for creating a [MediaThumbnailProvider] - const MediaThumbnailProvider({ - required this.media, - }); - - /// Media to load - final AssetEntity media; - - @override - ImageStreamCompleter load( - MediaThumbnailProvider key, - DecoderCallback decode, - ) => - MultiFrameImageStreamCompleter( - codec: _loadAsync(key, decode), - scale: 1, - informationCollector: () sync* { - yield ErrorDescription('Id: ${media.id}'); - }, - ); - - Future _loadAsync( - MediaThumbnailProvider key, - DecoderCallback decode, - ) async { - assert(key == this, 'Checks MediaThumbnailProvider'); - final bytes = await media.thumbnailData; - - return decode(bytes!); - } - - @override - Future obtainKey(ImageConfiguration configuration) => - SynchronousFuture(this); - - @override - bool operator ==(dynamic other) { - if (other.runtimeType != runtimeType) return false; - final MediaThumbnailProvider typedOther = other; - return media.id == typedOther.media.id; - } - - @override - int get hashCode => media.id.hashCode; - - @override - String toString() => '$runtimeType("${media.id}")'; -} - -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 4ade7890..00000000 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ /dev/null @@ -1,2080 +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; - -const _kDefaultMaxAttachmentSize = 20971520; // 20MB 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) { - // ignore: no-empty-block - channel.keyStroke(widget.parentMessage?.id).catchError((e) {}); - } - - 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 f577e8d9..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,132 +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; - -const _kDefaultMaxAttachmentSize = 20971520; // 20MB in Bytes +const _kCommandTrigger = '/'; +const _kMentionTrigger = '@'; /// Inactive state: /// @@ -179,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 [], @@ -193,15 +92,14 @@ class StreamMessageInput extends StatefulWidget { this.activeSendButton, this.showCommandsButton = true, this.userMentionsTileBuilder, - this.maxAttachmentSize = _kDefaultMaxAttachmentSize, + this.maxAttachmentSize = kDefaultMaxAttachmentSize, this.onError, this.attachmentLimit = 10, this.onAttachmentLimitExceed, this.attachmentButtonBuilder, this.commandButtonBuilder, - this.customOverlays = const [], + this.customAutocompleteTriggers = const [], this.mentionAllAppUsers = false, - this.attachmentsPickerBuilder, this.sendButtonBuilder, this.shouldKeepFocusAfterMessage, this.validator = _defaultValidator, @@ -210,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 @@ -235,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; @@ -290,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; @@ -334,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; @@ -356,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, ''); @@ -391,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 @@ -417,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!); @@ -427,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); } } @@ -444,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) { @@ -505,6 +427,7 @@ class StreamMessageInputState extends State ), ); } + return StreamMessageValueListenableBuilder( valueListenable: _effectiveController, builder: (context, value, _) { @@ -524,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) @@ -567,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( @@ -582,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(), + // ), ], ), ), @@ -599,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) { @@ -778,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) @@ -785,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, + ), + ), ), - ), - ], + ], + ), ), ), ), @@ -890,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, @@ -944,33 +907,30 @@ class StreamMessageInputState extends State value = value.trim(); final channel = StreamChannel.of(context).channel; - if (channel.ownCapabilities.contains(PermissionType.sendTypingEvents) && - value.isNotEmpty) { - channel - .keyStroke(_effectiveController.value.parentId) - // ignore: no-empty-block - .catchError((e) {}); + if (value.isNotEmpty && + channel.ownCapabilities.contains(PermissionType.sendTypingEvents)) { + // Notify the server that the user started typing. + channel.keyStroke(_effectiveController.message.parentId).onError( + (error, stackTrace) { + widget.onError?.call(error!, stackTrace); + }, + ); } var actionsLength = widget.actions.length; 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) { @@ -1052,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, ); } @@ -1272,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), @@ -1325,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) == @@ -1392,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, ), @@ -1417,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; - }); }, ); @@ -1447,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; @@ -1560,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, @@ -1670,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(); @@ -1704,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); @@ -1734,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 67% 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 31853b90..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, @@ -206,14 +117,14 @@ class StreamMessageListView extends StatefulWidget { this.paginationLimit = 20, this.paginationLoadingIndicatorBuilder, this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.onDrag, - this.spacingWidgetBuilder, + this.spacingWidgetBuilder = _defaultSpacingWidgetBuilder, }); /// [ScrollViewKeyboardDismissBehavior] the defines how this [PositionedList] will /// 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,24 +250,33 @@ 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) - final SpacingWidgetBuilder? spacingWidgetBuilder; + /// {@macro spacingWidgetBuilder} + final SpacingWidgetBuilder spacingWidgetBuilder; + + static Widget _defaultSpacingWidgetBuilder( + BuildContext context, + List spacingTypes, + ) { + if (!spacingTypes.contains(SpacingType.defaultSpacing)) { + return const SizedBox(height: 8); + } + return const SizedBox(height: 2); + } @override _StreamMessageListViewState createState() => _StreamMessageListViewState(); @@ -370,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; @@ -425,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 ?? @@ -457,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; } @@ -584,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 @@ -607,12 +616,9 @@ 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; - late Widget separator; if (widget.reverse) { message = messages[i - 1]; nextMessage = messages[i - 2]; @@ -621,88 +627,58 @@ class _StreamMessageListViewState extends State { nextMessage = messages[i - 1]; } + Widget separator; + + final isThread = message.replyCount! > 0; + if (!Jiffy(message.createdAt.toLocal()).isSame( nextMessage.createdAt.toLocal(), Units.DAY, )) { separator = _buildDateDivider(nextMessage); - } - final timeDiff = - Jiffy(nextMessage.createdAt.toLocal()).diff( - message.createdAt.toLocal(), - Units.MINUTE, - ); + } else { + final timeDiff = + Jiffy(nextMessage.createdAt.toLocal()).diff( + message.createdAt.toLocal(), + Units.MINUTE, + ); - final spacingRules = []; + final isNextUserSame = + message.user!.id == nextMessage.user?.id; + final isDeleted = message.isDeleted; + final hasTimeDiff = timeDiff >= 1; - final isNextUserSame = - message.user!.id == nextMessage.user?.id; - final isThread = message.replyCount! > 0; - final isDeleted = message.isDeleted; - final hasTimeDiff = timeDiff >= 1; + final spacingRules = [ + if (hasTimeDiff) SpacingType.timeDiff, + if (!isNextUserSame) SpacingType.otherUser, + if (isThread) SpacingType.thread, + if (isDeleted) SpacingType.deleted, + ]; - if (hasTimeDiff) { - spacingRules.add(SpacingType.timeDiff); + if (spacingRules.isEmpty) { + spacingRules.add(SpacingType.defaultSpacing); + } + + separator = widget.spacingWidgetBuilder.call( + context, + spacingRules, + ); } - if (!isNextUserSame) { - spacingRules.add(SpacingType.otherUser); - } - - if (isThread) { - spacingRules.add(SpacingType.thread); - } - - if (isDeleted) { - spacingRules.add(SpacingType.deleted); - } - - if (spacingRules.isNotEmpty) { - separator = widget.spacingWidgetBuilder - ?.call(context, spacingRules) ?? - const SizedBox(height: 8); - } - separator = widget.spacingWidgetBuilder - ?.call(context, [SpacingType.defaultSpacing]) ?? - const SizedBox(height: 2); - - if (!isThread && unreadCount > 0 && unreadCount == i - 1) { - final unreadMessagesSeparator = widget - .unreadMessagesSeparatorBuilder - ?.call(context, unreadCount); + if (!isThread && + unreadCount > 0 && + _oldestUnreadMessage?.id == nextMessage.id) { + final unreadMessagesSeparator = + _buildUnreadMessagesSeparator(unreadCount); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ separator, - unreadMessagesSeparator ?? - Padding( - padding: - const EdgeInsets.symmetric(vertical: 8), - child: DecoratedBox( - decoration: BoxDecoration( - gradient: - _streamTheme.colorTheme.bgGradient, - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: Text( - context.translations - .unreadMessagesSeparatorText( - unreadCount, - ), - textAlign: TextAlign.center, - style: - StreamChannelHeaderTheme.of(context) - .subtitleStyle, - ), - ), - ), - ), + unreadMessagesSeparator, ], ); } - return separator; }, itemBuilder: (context, i) { @@ -727,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, ); } @@ -794,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, + ), ], ); @@ -816,122 +803,19 @@ class _StreamMessageListViewState extends State { return child; } - 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 _buildUnreadMessagesSeparator(int unreadCount) { + final unreadMessagesSeparator = + widget.unreadMessagesSeparatorBuilder?.call(context, unreadCount) ?? + UnreadMessagesSeparator(unreadCount: unreadCount); + return unreadMessagesSeparator; } - 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) { @@ -958,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, @@ -1085,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(); @@ -1114,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) { @@ -1272,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(); @@ -1327,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; @@ -1351,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 = @@ -1463,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( @@ -1481,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 b247c91e..00000000 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ /dev/null @@ -1,1491 +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, - }) : 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, - ), - ), - 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, - ), - 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; - - /// 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, - }) => - 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, - ); - - @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( - clipBehavior: Clip.hardEdge, - 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 1f720db2..7b5940c5 100644 --- a/packages/stream_chat_flutter/lib/src/system_message.dart +++ b/packages/stream_chat_flutter/lib/src/misc/system_message.dart @@ -1,36 +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'; -/// {@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 + /// The action to perform when tapping on the message. final void Function(Message)? onMessageTap; @override Widget build(BuildContext context) { final theme = StreamChatTheme.of(context); + final message = this.message.replaceMentions(linkify: false); + + final messageText = message.text; + if (messageText == null) return const SizedBox.shrink(); + return GestureDetector( behavior: HitTestBehavior.opaque, onTap: onMessageTap == null ? null : () => onMessageTap!(message), child: Text( - message.text!, + messageText, textAlign: TextAlign.center, softWrap: true, style: theme.textTheme.captionBold.copyWith( 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 82% 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 129875cf..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), @@ -319,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, @@ -336,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 54bd7191..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(); + }, + ), ), - ), - ); - }, + ); + }, + ), ), ), ); @@ -140,11 +164,10 @@ class StreamChatState extends State { @override void didChangeDependencies() { - final currentLocale = Localizations.localeOf(context); - final languageCode = currentLocale.languageCode; + final currentLocale = Localizations.localeOf(context).toString(); final availableLocales = Jiffy.getAllAvailableLocales(); - if (availableLocales.contains(languageCode)) { - Jiffy.locale(languageCode); + if (availableLocales.contains(currentLocale)) { + Jiffy.locale(currentLocale); } super.didChangeDependencies(); } 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 58% rename from packages/stream_chat_flutter/lib/src/extension.dart rename to packages/stream_chat_flutter/lib/src/utils/extensions.dart index 5ad30039..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. @@ -31,6 +34,46 @@ extension StringExtension on String { /// Levenshtein distance between this and [t]. int levenshteinDistance(String t) => levenshtein(this, t); + + /// Returns a resized imageUrl with the given [width], [height], [resize] + /// and [crop] if it is from Stream CDN or Dashboard. + /// + /// Read more at https://getstream.io/chat/docs/flutter-dart/file_uploads/?language=dart#image-resizing + String getResizedImageUrl({ + // TODO: Are these sizes optimal? Consider web/desktop + double width = 400, + double height = 400, + String /*clip|crop|scale|fill*/ resize = 'clip', + String /*center|top|bottom|left|right*/ crop = 'center', + }) { + final uri = Uri.parse(this); + final host = uri.host; + + final fromStreamCDN = host.endsWith('stream-io-cdn.com'); + final fromStreamDashboard = host.endsWith('stream-cloud-uploads.imgix.net'); + + if (!fromStreamCDN && !fromStreamDashboard) return this; + + final queryParameters = {...uri.queryParameters}; + + if (fromStreamCDN) { + if (queryParameters['h'].isNullOrMatches('*') && + queryParameters['w'].isNullOrMatches('*') && + queryParameters['crop'].isNullOrMatches('*') && + queryParameters['resize'].isNullOrMatches('*')) { + queryParameters['h'] = height.floor().toString(); + queryParameters['w'] = width.floor().toString(); + queryParameters['crop'] = crop; + queryParameters['resize'] = resize; + } + } else if (fromStreamDashboard) { + queryParameters['height'] = height.floor().toString(); + queryParameters['width'] = width.floor().toString(); + queryParameters['fit'] = crop; + } + + return uri.replace(queryParameters: queryParameters).toString(); + } } /// List extension @@ -45,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] @@ -156,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 @@ -281,3 +385,54 @@ extension UriX on Uri { return Uri.parse('http://${toString()}'); } } + +/// Extensions on generic type [T] +extension TypeX on T? { + /// Returns true if the value is null or matches the given [value] + /// otherwise returns false. + bool isNullOrMatches(T value) => this == null || this == value; +} + +/// 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 bea16577..00000000 --- a/packages/stream_chat_flutter/lib/src/v4/message_input/stream_attachment_picker.dart +++ /dev/null @@ -1,564 +0,0 @@ -import 'dart:async'; - -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/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 = 20971520, - this.onError, - this.allowedAttachmentTypes = const [ - DefaultAttachmentTypes.image, - DefaultAttachmentTypes.file, - DefaultAttachmentTypes.video, - ], - this.customAttachmentTypes = const [], - }); - - /// 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; - - /// 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 = const [], - }) => - 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, - ); - - @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, - ), - ), - ), - ], - ), - ), - ), - ), - ); - } - - 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 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, - }); - - 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; - - @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, - ); - } - - 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 5052393a..b5e44c56 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -1,77 +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_tile.dart'; -export 'src/v4/scroll_view/user_scroll_view/stream_user_grid_view.dart'; -export 'src/v4/scroll_view/user_scroll_view/stream_user_grid_view.dart'; -export 'src/v4/scroll_view/user_scroll_view/stream_user_list_tile.dart'; -export 'src/v4/scroll_view/user_scroll_view/stream_user_list_view.dart'; -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 478be89d..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.3.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: ^4.1.3 + file_selector: ^0.9.0 flutter: sdk: flutter flutter_markdown: ^0.6.1 flutter_portal: ^1.0.0 - flutter_slidable: ^1.2.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.3.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 b986bbd6..96c2a8ed 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,41 @@ +## 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). + +## 4.5.0 + +- Updated `stream_chat` dependency to [`4.5.0`](https://pub.dev/packages/stream_chat/changelog). +- [#1269](https://github.com/GetStream/stream-chat-flutter/issues/1269) + Fix `ChannelListEventHandler` castError at PagedValue.asSuccess. +- [#1241](https://github.com/GetStream/stream-chat-flutter/issues/1241) StreamChannelListView load + more indicator non stop. + +## 4.4.1 + +- Updated `stream_chat` dependency to [`4.4.1`](https://pub.dev/packages/stream_chat/changelog). + +## 4.4.0 + +- Updated `stream_chat` dependency to [`4.4.0`](https://pub.dev/packages/stream_chat/changelog). + ## 4.3.0 - Updated `stream_chat` dependency to [`4.3.0`](https://pub.dev/packages/stream_chat/changelog). @@ -8,8 +46,10 @@ 🔄 Changed -- Deprecated `before` and `after` parameters in `StreamChannel.queryAroundMessage`. Use `limit` instead. -- Deprecated `before` and `after` parameters in `StreamChannel.loadChannelAtMessage`. Use `limit` instead. +- Deprecated `before` and `after` parameters in `StreamChannel.queryAroundMessage`. Use `limit` + instead. +- Deprecated `before` and `after` parameters in `StreamChannel.loadChannelAtMessage`. Use `limit` + instead. ## 4.1.0 @@ -22,14 +62,17 @@ ## 4.0.0 -For upgrading to V4, please refer to the [V4 Migration Guide](https://getstream.io/chat/docs/sdk/flutter/guides/migration_guide_4_0/) +For upgrading to V4, please refer to +the [V4 Migration Guide](https://getstream.io/chat/docs/sdk/flutter/guides/migration_guide_4_0/) - Deprecated `UsersBloc` in favor of `StreamUserListController` to control the user list. -- Deprecated `MessageSearchBloc` in favor of `StreamMessageSearchListController` to control the user list. +- Deprecated `MessageSearchBloc` in favor of `StreamMessageSearchListController` to control the user + list. ## 4.0.0-beta.2 -- Updated `stream_chat` dependency to [`4.0.0-beta.2`](https://pub.dev/packages/stream_chat/changelog). +- Updated `stream_chat` dependency + to [`4.0.0-beta.2`](https://pub.dev/packages/stream_chat/changelog). ## 4.0.0-beta.0 @@ -40,7 +83,8 @@ For upgrading to V4, please refer to the [V4 Migration Guide](https://getstream. - Deprecated `ChannelsBloc` in favor of `StreamChannelListController` to control the channel list. - Added `MessageTextFieldController` to be used with the new `StreamTextField` ui widget. -- Updated `stream_chat` dependency to [`4.0.0-beta.0`](https://pub.dev/packages/stream_chat/changelog). +- Updated `stream_chat` dependency + to [`4.0.0-beta.0`](https://pub.dev/packages/stream_chat/changelog). ## 3.6.1 @@ -59,12 +103,14 @@ For upgrading to V4, please refer to the [V4 Migration Guide](https://getstream. - Updated `stream_chat` dependency to [`3.5.0`](https://pub.dev/packages/stream_chat/changelog). ## 3.4.0 + - Updated `stream_chat` dependency to [`3.4.0`](https://pub.dev/packages/stream_chat/changelog). 🐞 Fixed - Do not move a channel to top if the new message is from a thread. -- [[#848]](https://github.com/GetStream/stream-chat-flutter/issues/848) Fixed "Bad state: Cannot add new events after calling close" by replacing all `.add` methods with a new `.safeAdd`. +- [[#848]](https://github.com/GetStream/stream-chat-flutter/issues/848) Fixed "Bad state: Cannot add + new events after calling close" by replacing all `.add` methods with a new `.safeAdd`. ## 3.3.1 @@ -125,8 +171,8 @@ For upgrading to V4, please refer to the [V4 Migration Guide](https://getstream. 🐞 Fixed - Fixed `MessageSearchBloc` pagination. -- [[#673]](https://github.com/GetStream/stream-chat-flutter/issues/673): Fix `Core Widgets` not getting rebuild with new - data on configuration change. +- [[#673]](https://github.com/GetStream/stream-chat-flutter/issues/673): Fix `Core Widgets` not + getting rebuild with new data on configuration change. ## 2.2.1 @@ -144,8 +190,8 @@ For upgrading to V4, please refer to the [V4 Migration Guide](https://getstream. 🐞 Fixed -- [#612](https://github.com/GetStream/stream-chat-flutter/issues/612) `ChannelListView` pagination doesn't work after - refresh +- [#612](https://github.com/GetStream/stream-chat-flutter/issues/612) `ChannelListView` pagination + doesn't work after refresh ## 2.1.1 @@ -163,20 +209,23 @@ For upgrading to V4, please refer to the [V4 Migration Guide](https://getstream. 🔄 Changed -- `StreamChatCore.of(context).user` is now deprecated in favor of `StreamChatCore.of(context).currentUser`. -- `StreamChatCore.of(context).userStream` is now deprecated in favor of `StreamChatCore.of(context).currentUserStream`. +- `StreamChatCore.of(context).user` is now deprecated in favor + of `StreamChatCore.of(context).currentUser`. +- `StreamChatCore.of(context).userStream` is now deprecated in favor + of `StreamChatCore.of(context).currentUserStream`. ## 2.0.0 🛑️ Breaking Changes from `1.5.3` - migrate this package to null safety -- `channelsBloc.queryChannels()`, `ChannelListCore` options param/property is removed in favor of individual - params/properties +- `channelsBloc.queryChannels()`, `ChannelListCore` options param/property is removed in favor of + individual params/properties - `options.state` -> bool state - `options.watch` -> bool watch - `options.presence` -> bool presence -- `usersBloc.queryUsers()`, `UserListCore` options param/property is removed in favor of individual params/properties +- `usersBloc.queryUsers()`, `UserListCore` options param/property is removed in favor of individual + params/properties - `options.presence` -> bool presence ✅ Added @@ -196,12 +245,13 @@ For upgrading to V4, please refer to the [V4 Migration Guide](https://getstream. 🛑️ Breaking Changes from `2.0.0-nullsafety.7` -- `channelsBloc.queryChannels()`, `ChannelListCore` options param/property is removed in favor of individual - params/properties +- `channelsBloc.queryChannels()`, `ChannelListCore` options param/property is removed in favor of + individual params/properties - `options.state` -> bool state - `options.watch` -> bool watch - `options.presence` -> bool presence -- `usersBloc.queryUsers()`, `UserListCore` options param/property is removed in favor of individual params/properties +- `usersBloc.queryUsers()`, `UserListCore` options param/property is removed in favor of individual + params/properties - `options.presence` -> bool presence ## 2.0.0-nullsafety.7 diff --git a/packages/stream_chat_flutter_core/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 ce91a8f4..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.3.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/lazy_load_scroll_view.dart b/packages/stream_chat_flutter_core/lib/src/lazy_load_scroll_view.dart index e086ab55..d56d5f23 100644 --- a/packages/stream_chat_flutter_core/lib/src/lazy_load_scroll_view.dart +++ b/packages/stream_chat_flutter_core/lib/src/lazy_load_scroll_view.dart @@ -17,6 +17,7 @@ class LazyLoadScrollView extends StatefulWidget { this.onPageScrollEnd, this.onInBetweenOfPage, this.scrollOffset = 100, + this.allowNotificationBubbling = false, }); /// The [Widget] that this widget watches for changes on @@ -40,6 +41,9 @@ class LazyLoadScrollView extends StatefulWidget { /// The offset to take into account when triggering [onEndOfPage]/[onStartOfPage] in pixels final double scrollOffset; + /// If true the notifications will keep bubbling up the tree + final bool allowNotificationBubbling; + @override State createState() => _LazyLoadScrollViewState(); } @@ -59,13 +63,13 @@ class _LazyLoadScrollViewState extends State { if (notification is ScrollStartNotification) { if (widget.onPageScrollStart != null) { widget.onPageScrollStart!(); - return true; + return !widget.allowNotificationBubbling; } } if (notification is ScrollEndNotification) { if (widget.onPageScrollEnd != null) { widget.onPageScrollEnd!(); - return true; + return !widget.allowNotificationBubbling; } } if (notification is ScrollUpdateNotification) { @@ -78,7 +82,7 @@ class _LazyLoadScrollViewState extends State { pixels < (maxScrollExtent - scrollOffset)) { if (widget.onInBetweenOfPage != null) { widget.onInBetweenOfPage!(); - return true; + return !widget.allowNotificationBubbling; } } @@ -90,12 +94,12 @@ class _LazyLoadScrollViewState extends State { if (scrollingDown) { if (extentAfter <= scrollOffset) { _onEndOfPage(); - return true; + return !widget.allowNotificationBubbling; } } else { if (extentBefore <= scrollOffset) { _onStartOfPage(); - return true; + return !widget.allowNotificationBubbling; } } } @@ -106,7 +110,7 @@ class _LazyLoadScrollViewState extends State { if (notification.overscroll < 0) { _onStartOfPage(); } - return true; + return !widget.allowNotificationBubbling; } return false; } 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.dart b/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.dart index 9525fea4..c122be1d 100644 --- a/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.dart +++ b/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.dart @@ -111,6 +111,9 @@ abstract class PagedValue with _$PagedValue { /// Returns `true` if the [PagedValue] is [Success]. bool get isSuccess => this is Success; + /// Returns `true` if the [PagedValue] is not [Success]. + bool get isNotSuccess => !isSuccess; + /// Returns the [PagedValue] as [Success]. Success get asSuccess { assert( diff --git a/packages/stream_chat_flutter_core/lib/src/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 61193ac2..7cd425a6 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart @@ -163,13 +163,14 @@ class StreamChannelListController extends PagedValueNotifier { } } - /// Replaces the previously loaded channels with [channels] and updates - /// the nextPageKey. + /// Replaces the previously loaded channels with the passed [channels]. set channels(List channels) { - value = PagedValue( - items: channels, - nextPageKey: channels.length, - ); + if (value.isSuccess) { + final currentValue = value.asSuccess; + value = currentValue.copyWith(items: channels); + } else { + value = PagedValue(items: channels); + } } /// Returns/Creates a new Channel and starts watching it. @@ -223,6 +224,9 @@ class StreamChannelListController extends PagedValueNotifier { client.on().skip(1) // Skipping the last emitted event. // We only need to handle the latest events. .listen((event) { + // Only handle the event if the value is in success state. + if (value.isNotSuccess) return; + // Returns early if the event is already handled by the listener. if (eventListener?.call(event) ?? false) return; 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/stream_user_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_user_list_controller.dart index 3e1b2892..466b6177 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_user_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_user_list_controller.dart @@ -153,12 +153,13 @@ class StreamUserListController extends PagedValueNotifier { return super.refresh(resetValue: resetValue); } - /// Replaces the previously loaded users with [users] and updates - /// the nextPageKey. + /// Replaces the previously loaded users with the passed [users]. set users(List users) { - value = PagedValue( - items: users, - nextPageKey: users.length, - ); + if (value.isSuccess) { + final currentValue = value.asSuccess; + value = currentValue.copyWith(items: users); + } else { + value = PagedValue(items: users); + } } } 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 3365e724..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.3.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.3.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 1c2bc12b..20c9aaef 100644 --- a/packages/stream_chat_localizations/CHANGELOG.md +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -1,3 +1,29 @@ +## 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. + ## 3.2.0 ✅ Added diff --git a/packages/stream_chat_localizations/README.md b/packages/stream_chat_localizations/README.md index 24805d6e..b55eb7ad 100644 --- a/packages/stream_chat_localizations/README.md +++ b/packages/stream_chat_localizations/README.md @@ -39,6 +39,7 @@ At the moment we support the following languages: - [Korean](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart) - [Portuguese](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart) - [German](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart) +- [Norwegian](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart) More languages will be added in the future. Feel free to [contribute](https://github.com/GetStream/stream-chat-flutter/blob/master/CONTRIBUTING.md) to add more languages. @@ -77,6 +78,8 @@ class MyApp extends StatelessWidget { Locale('ja'), Locale('ko'), Locale('pt'), + Locale('de'), + Locale('no'), ], // Add GlobalStreamChatLocalizations.delegates localizationsDelegates: GlobalStreamChatLocalizations.delegates, @@ -123,6 +126,8 @@ Example: ja ko pt + de + no ``` diff --git a/packages/stream_chat_localizations/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 3087e900..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,23 +3,16 @@ 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. /// @@ -39,6 +32,7 @@ const kStreamChatSupportedLanguages = { 'ko', 'pt', 'de', + 'no', }; /// Creates a [GlobalStreamChatLocalizations] instance for the given `locale`. @@ -79,6 +73,8 @@ GlobalStreamChatLocalizations? getStreamChatTranslation(Locale locale) { return const StreamChatLocalizationsPt(); case 'de': return const StreamChatLocalizationsDe(); + case 'no': + return const StreamChatLocalizationsNo(); default: return null; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_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 new file mode 100644 index 00000000..db231a86 --- /dev/null +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart @@ -0,0 +1,438 @@ +part of 'stream_chat_localizations.dart'; + +/// The translations for Norwegian (`no`). +class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { + /// Create an instance of the translation bundle for Norwegian. + const StreamChatLocalizationsNo({super.localeName = 'no'}); + + @override + String get launchUrlError => 'Kan ikke laste inn url'; + + @override + String get loadingUsersError => 'Problem med å laste inn brukere'; + + @override + String get noUsersLabel => 'Det er ingen brukere akkurat nå'; + + @override + String get retryLabel => 'Prøv igjen'; + + @override + String get userLastOnlineText => 'Sist pålogget'; + + @override + String get userOnlineText => 'Pålogget'; + + @override + String userTypingText(Iterable users) { + if (users.isEmpty) return ''; + final first = users.first; + if (users.length == 1) { + return '${first.name} skriver'; + } + return '${first.name} og ${users.length - 1} flere skriver'; + } + + @override + String get threadReplyLabel => 'Svar på tråd'; + + @override + String get onlyVisibleToYouText => 'Kun synlig for deg'; + + @override + String threadReplyCountText(int count) => '$count svar på tråd'; + + @override + String attachmentsUploadProgressText({ + required int remaining, + required int total, + }) => + 'Laster opp $remaining/$total ...'; + + @override + String pinnedByUserText({ + required User pinnedBy, + required User currentUser, + }) { + final pinnedByCurrentUser = currentUser.id == pinnedBy.id; + if (pinnedByCurrentUser) return 'Festet av deg'; + return 'Festet av ${pinnedBy.name}'; + } + + @override + String get sendMessagePermissionError => + 'Du har ikke tillatelse til å sende meldinger'; + + @override + String get emptyMessagesText => 'Det er ingen meldinger akkurat nå'; + + @override + String get genericErrorText => 'Noe gikk galt'; + + @override + String get loadingMessagesError => 'Problem med å laste inn meldinger'; + + @override + String resultCountText(int count) => '$count resultater'; + + @override + String get messageDeletedText => 'Denne meldingen er slettet.'; + + @override + String get messageDeletedLabel => 'Melding slettet'; + + @override + String get messageReactionsLabel => 'Reaksjoner på melding'; + + @override + String get emptyChatMessagesText => 'Ingen meldinger her enda...'; + + @override + String threadSeparatorText(int replyCount) { + if (replyCount == 1) return '1 svar'; + return '$replyCount svar'; + } + + @override + String get connectedLabel => 'Tilkoblet'; + + @override + String get disconnectedLabel => 'Avbrutt'; + + @override + String get reconnectingLabel => 'Prøver å koble til...'; + + @override + String get alsoSendAsDirectMessageLabel => 'Også send som en direktemelding'; + + @override + String get addACommentOrSendLabel => 'Legg til en kommentar eller send'; + + @override + String get searchGifLabel => 'Søk GIFs'; + + @override + String get writeAMessageLabel => 'Skriv en melding'; + + @override + String get instantCommandsLabel => 'Direkte kommandoer'; + + @override + String fileTooLargeAfterCompressionError(double limitInMB) => + 'Filen er for stor til å laste opp. ' + 'Grensen for filopplasting er $limitInMB MB. ' + 'Vi prøvde å komprimere den, men det hjalp ikke.'; + + @override + String fileTooLargeError(double limitInMB) => + 'Filen er for stor til å laste opp. Filgrense er $limitInMB MB.'; + + @override + String get addAFileLabel => 'Legg til en fil'; + + @override + String get photoFromCameraLabel => 'Bilde fra kamera'; + + @override + String get uploadAFileLabel => 'Last opp en fil'; + + @override + String get uploadAPhotoLabel => 'Last opp et bilde'; + + @override + String get uploadAVideoLabel => 'Last opp en video'; + + @override + String get videoFromCameraLabel => 'Video fra kamera'; + + @override + String get okLabel => 'OK'; + + @override + String get somethingWentWrongError => 'Noe gikk galt'; + + @override + String get addMoreFilesLabel => 'Legg til flere filer'; + + @override + String get enablePhotoAndVideoAccessMessage => + 'Vennligst gi tillatelse til dine bilder' + '\nog videoer så du kan dele de med dine venner.'; + + @override + String get allowGalleryAccessMessage => 'Tillat tilgang til galleri'; + + @override + String get flagMessageLabel => 'Rapporter melding'; + + @override + String get flagMessageQuestion => + 'Ønsker du å sende en kopi av denne meldingen til en' + '\nmoderator for videre undersøkelser'; + + @override + String get flagLabel => 'RAPPORTER'; + + @override + String get cancelLabel => 'AVBRYT'; + + @override + String get flagMessageSuccessfulLabel => 'Melding rapportert'; + + @override + String get flagMessageSuccessfulText => + 'Meldingen har blitt rapportert til en moderator.'; + + @override + String get deleteLabel => 'SLETT'; + + @override + String get deleteMessageLabel => 'Slett melding'; + + @override + String get deleteMessageQuestion => + 'Er du sikker på at du ønsker å slette denne meldingen permanent?'; + + @override + String get operationCouldNotBeCompletedText => + 'Denne handlingen kunne ikke bli gjennomført.'; + + @override + String get replyLabel => 'Svar'; + + @override + String togglePinUnpinText({required bool pinned}) { + if (pinned) return 'Løsne fra samtale'; + return 'Fest til samtale'; + } + + @override + String toggleDeleteRetryDeleteMessageText({required bool isDeleteFailed}) { + if (isDeleteFailed) return 'Prøv å slett melding på nytt'; + return 'Slett melding'; + } + + @override + String get copyMessageLabel => 'Kopier melding'; + + @override + String get editMessageLabel => 'Rediger melding'; + + @override + String toggleResendOrResendEditedMessage({required bool isUpdateFailed}) { + if (isUpdateFailed) return 'Send redigert melding på nytt'; + return 'Send på nytt'; + } + + @override + String get photosLabel => 'Foto'; + + String _getDay(DateTime dateTime) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final yesterday = DateTime(now.year, now.month, now.day - 1); + + final date = DateTime(dateTime.year, dateTime.month, dateTime.day); + + if (date == today) { + return 'i dag'; + } else if (date == yesterday) { + return 'i går'; + } else { + return 'på ${Jiffy(date).MMMd}'; + } + } + + @override + String sentAtText({required DateTime date, required DateTime time}) => + 'Sent ${_getDay(date)} kl. ${Jiffy(time.toLocal()).format('HH:mm')}'; + + @override + String get todayLabel => 'I dag'; + + @override + String get yesterdayLabel => 'I går'; + + @override + String get channelIsMutedText => 'Kanal er dempet'; + + @override + String get noTitleText => 'Ingen tittel'; + + @override + String get letsStartChattingLabel => 'La oss starte å chatte!'; + + @override + String get sendingFirstMessageLabel => + 'Hva med å sende din første melding til en venn?'; + + @override + String get startAChatLabel => 'Start en chat'; + + @override + String get loadingChannelsError => 'Problemer med å laste inn kanaler'; + + @override + String get deleteConversationLabel => 'Slett samtale'; + + @override + String get deleteConversationQuestion => + 'Er du sikker på at du ønsker å slette denne samtalen?'; + + @override + String get streamChatLabel => 'Stream Chat'; + + @override + String get searchingForNetworkText => 'Søker etter nettverk'; + + @override + String get offlineLabel => 'Avlogget...'; + + @override + String get tryAgainLabel => 'Prøv igjen'; + + @override + String membersCountText(int count) { + if (count == 1) return '1 medlem'; + return '$count medlemmer'; + } + + @override + String watchersCountText(int count) { + if (count == 1) return '1 pålogget'; + return '$count pålogget'; + } + + @override + String get viewInfoLabel => 'Se info'; + + @override + String get leaveGroupLabel => 'Forlat gruppe'; + + @override + String get leaveLabel => 'FORLAT'; + + @override + String get leaveConversationLabel => 'Forlat samtale'; + + @override + String get leaveConversationQuestion => + 'Er du sikker på at du ønsker å forlate denne samtalen?'; + + @override + String get showInChatLabel => 'Se i chat'; + + @override + String get saveImageLabel => 'Lagre bilde'; + + @override + String get saveVideoLabel => 'Lagre video'; + + @override + String get uploadErrorLabel => 'PROBLEM MED OPPLASTNING'; + + @override + String get giphyLabel => 'Giphy'; + + @override + String get shuffleLabel => 'Stokk om'; + + @override + String get sendLabel => 'Send'; + + @override + String get withText => 'med'; + + @override + String get inText => 'i'; + + @override + String get youText => 'Du'; + + @override + String galleryPaginationText({ + required int currentPage, + required int totalPages, + }) => + '${currentPage + 1} of $totalPages'; + + @override + String get fileText => 'Fil'; + + @override + String get replyToMessageLabel => 'Svar på melding'; + + @override + String attachmentLimitExceedError(int limit) => + 'Antall vedlegg oversteget, maks antall: $limit'; + + @override + String get slowModeOnLabel => 'Sakte modus PÅ'; + + @override + String get linkDisabledDetails => + 'Sende lenker er ikke lov i denne samtalen.'; + + @override + String get linkDisabledError => 'Lenker er deaktivert'; + + @override + String get viewLibrary => 'Se bibliotek'; + + @override + String unreadMessagesSeparatorText(int unreadCount) { + if (unreadCount == 1) { + return '1 ulest melding'; + } + return '$unreadCount uleste meldinger'; + } + + @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 caa85279..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.2.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.3.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 4c9bea04..f85d13b7 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,3 +1,20 @@ +## 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`. + Thanks [geweald](https://github.com/geweald). + +## 4.3.0 + +- Updated `stream_chat` dependency to [`4.4.0`](https://pub.dev/packages/stream_chat/changelog). + ## 4.2.0 - Added support for `Channel.ownCapabilities` diff --git a/packages/stream_chat_persistence/example/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 26830534..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.3.0 - stream_chat_persistence: ^4.2.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/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/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index 065be5cc..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.2.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.3.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(),