> ## Documentation Index
> Fetch the complete documentation index at: https://cometchat-013b37f0.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Shortcut Formatter

## Introduction

The `ShortcutFormatter` class extends the `CometChatTextFormatter` class to provide a mechanism for handling shortcuts within messages. This guide will walk you through the process of using ShortcutFormatter to implement shortcut extensions in your CometChat application.

## Setup

1. **Create the ShortcutFormatter Class**: Define the `ShortcutFormatter` class by extending the `CometChatTextFormatter` class.

<Tabs>
  <Tab title="Dart">
    ```dart theme={null}
    import 'package:cometchat_chat_uikit/cometchat_chat_uikit.dart';

    class ShortcutFormatter extends CometChatTextFormatter {

    }
    ```
  </Tab>
</Tabs>

2. **Init**: Initialize the `messageShortcuts` map and `shortcuts` list in the init().

<Tabs>
  <Tab title="Dart">
    ```dart theme={null}
    @override
    void init() {
      trackingCharacter ??= "!";
    }
    ```
  </Tab>
</Tabs>

3. **Prepare Shortcuts**: Implement the `prepareShortcuts()` method to fetch shortcuts from the server using CometChat extension.

<Tabs>
  <Tab title="Dart">
    ```dart theme={null}
    bool isShortcutTracking = false;

    void prepareShortcuts(TextEditingController textEditingController) {
      CometChat.callExtension('message-shortcuts', 'GET', '/v1/fetch', null,
          onSuccess: (map) {
        if (map.isNotEmpty) {
          Map<String, dynamic> data = map["data"];
          if (data.isNotEmpty) {
            Map<String, dynamic> shortcuts = data["shortcuts"];
            if (shortcuts.isNotEmpty) {
                parseData(
                    shortcuts: shortcuts,
                    textEditingController: textEditingController
                );
            }
          }
        }
      }, onError: (error) {});
    }

    void parseData({Map<String, dynamic>? shortcuts, required TextEditingController textEditingController}) async {
      if (shortcuts == null || shortcuts.isEmpty) {
        suggestionListEventSink?.add([]);
        if (onSearch != null) {
          onSearch!(null);
        }
        CometChatUIEvents.hidePanel(composerId, CustomUIPosition.composerPreview);
      } else {
        CometChatUIEvents.hidePanel(composerId, CustomUIPosition.composerPreview);
        if (suggestionListEventSink != null && shortcuts.isNotEmpty) {
          List<SuggestionListItem> list = [];
          shortcuts.forEach((key, value) => list.add(SuggestionListItem(
              id: key,
              title: "$key → $value",
              onTap: () {
                int cursorPos = textEditingController.selection.base.offset;
                String textOnLeftOfValue = textEditingController.text.substring(0, cursorPos - 1);
                String textOnRightOfValue = textEditingController.text.substring(cursorPos);
                textEditingController.text = "$textOnLeftOfValue$value $textOnRightOfValue";
                updatePreviousText(textEditingController.text);
                textEditingController.selection = TextSelection(
                  baseOffset: cursorPos - 1 + "$value".length + 1,
                  extentOffset: cursorPos - 1 + "$value".length + 1,
                );
                resetMatchTracker();
                isShortcutTracking = false;
                CometChatUIEvents.hidePanel(
                    composerId, CustomUIPosition.composerPreview
                );
              })
          ));
          suggestionListEventSink?.add(list);
        }
      }
    }

    void updatePreviousText(String text) {
      if (previousTextEventSink != null) {
        previousTextEventSink!.add(text);
      }
    }

    void resetMatchTracker() {
      suggestionListEventSink?.add([]);
      if (onSearch != null) {
        onSearch!(null);
      }
    }
    ```
  </Tab>
</Tabs>

4. **Override onChange Method**: Override the `onChange()` method to search for shortcuts based on the entered query.

<Tabs>
  <Tab title="Dart">
    ```dart theme={null}
    @override
    void onChange(TextEditingController textEditingController, String previousText) {
      var cursorPosition = textEditingController.selection.base.offset;
      if (textEditingController.text.isEmpty) {
        resetMatchTracker();
        if (previousText.length > textEditingController.text.length) {
          if (previousText[cursorPosition] == trackingCharacter && isShortcutTracking) {
            isShortcutTracking = false;
            if (onSearch != null) {
              onSearch!(null);
            }
            CometChatUIEvents.hidePanel(composerId, CustomUIPosition.composerPreview);
          }
          return;
        }
      }

      if (previousText.length > textEditingController.text.length) {
        if (previousText[cursorPosition] == trackingCharacter) {
          isShortcutTracking = false;
          if (onSearch != null) {
            onSearch!(null);
          }
          CometChatUIEvents.hidePanel(composerId, CustomUIPosition.composerPreview);
        }
      } else {
        String previousCharacter = cursorPosition == 0 ? "" : textEditingController.text[cursorPosition - 1];
        bool isSpace = cursorPosition == 1 || (textEditingController.text.length > 1 && cursorPosition > 1 && (textEditingController.text[cursorPosition - 2] == " " || textEditingController.text[cursorPosition - 2] == "\n"));

        if (isShortcutTracking) {
          isShortcutTracking = false;
          if (onSearch != null) {
            onSearch!(null);
          }
          CometChatUIEvents.hidePanel(composerId, CustomUIPosition.composerPreview);
        } else if (previousCharacter == trackingCharacter && isSpace) {
          isShortcutTracking = true;
          if (onSearch != null) {
            onSearch!(trackingCharacter);
          }
          CometChatUIEvents.showPanel(composerId, CustomUIPosition.composerPreview, (context) => getLoadingIndicator(context, cometChatTheme));
          prepareShortcuts(textEditingController);
        }
      }
    }
    ```
  </Tab>
</Tabs>

5. **Handle Scroll to Bottom**: Override the `onScrollToBottom()` method if needed.

<Tabs>
  <Tab title="Dart">
    ```dart theme={null}
    @override
    void onScrollToBottom(TextEditingController textEditingController) {
      // TODO: implement onScrollToBottom
    }
    ```
  </Tab>
</Tabs>

***

## Usage

The widgets [CometChatConversations](/ui-kit/flutter/v4/conversations), [CometChatMessageComposer](/ui-kit/flutter/v4/message-composer) and [CometChatMessageList](/ui-kit/flutter/v4/message-list) have a property called `textFormatters` which accepts a list of CometChatTextFormatter to format the text. The code shared below `textFormatters` consisting of the above created Shortcuts formatter being passed down to [CometChatMessageComposer](/ui-kit/flutter/v4/message-composer) from [CometChatConversationsWithMessages](/ui-kit/flutter/v4/conversations-with-messages) with help of configurations.

<Tabs>
  <Tab title="Dart">
    ```dart theme={null}
    import 'package:cometchat_chat_uikit/cometchat_chat_uikit.dart';
    import 'package:flutter/material.dart';
    import 'shortcut_formatter.dart';

    class ShortcutFormatterExample extends StatefulWidget {
      const ShortcutFormatterExample({super.key});

      @override
      State<ShortcutFormatterExample> createState() => _ShortcutFormatterExampleState();
    }

    class _ShortcutFormatterExampleState extends State<ShortcutFormatterExample> {

      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: SafeArea(
            child: CometChatConversationsWithMessages(
                messageConfiguration: MessageConfiguration(
                  messageComposerConfiguration: MessageComposerConfiguration(
                    textFormatters: [ShortcutFormatter()],
                  ),
                )
            ),
          ),
        );
      }
    }
    ```
  </Tab>
</Tabs>

***

## Example

Here is the complete example for reference:

<Tabs>
  <Tab title="Dart">
    ```dart theme={null}
    import 'package:cometchat_chat_uikit/cometchat_chat_uikit.dart';
    import 'package:flutter/material.dart';

    class ShortcutFormatter extends CometChatTextFormatter {
      @override
      void init() {
        trackingCharacter ??= "!";
      }

      bool isShortcutTracking = false;

      void prepareShortcuts(TextEditingController textEditingController) {
        CometChat.callExtension('message-shortcuts', 'GET', '/v1/fetch', null,
            onSuccess: (map) {
              if (map.isNotEmpty) {
                Map<String, dynamic> data = map["data"];
                if (data.isNotEmpty) {
                  Map<String, dynamic> shortcuts = data["shortcuts"];
                  if (shortcuts.isNotEmpty) {
                    parseData(
                        shortcuts: shortcuts,
                        textEditingController: textEditingController);
                  }
                }
              }
            }, onError: (error) {});
      }

      void parseData({Map<String, dynamic>? shortcuts, required TextEditingController textEditingController}) async {
        if (shortcuts == null || shortcuts.isEmpty) {
          suggestionListEventSink?.add([]);
          if (onSearch != null) {
            onSearch!(null);
          }
          CometChatUIEvents.hidePanel(composerId, CustomUIPosition.composerPreview);
        } else {
          CometChatUIEvents.hidePanel(composerId, CustomUIPosition.composerPreview);
          if (suggestionListEventSink != null && shortcuts.isNotEmpty) {
            List<SuggestionListItem> list = [];
            shortcuts.forEach((key, value) => list.add(SuggestionListItem(
                id: key,
                title: "$key → $value",
                onTap: () {
                  int cursorPos = textEditingController.selection.base.offset;
                  String textOnLeftOfValue = textEditingController.text.substring(0, cursorPos - 1);
                  String textOnRightOfValue = textEditingController.text.substring(cursorPos);
                  textEditingController.text = "$textOnLeftOfValue$value $textOnRightOfValue";
                  updatePreviousText(textEditingController.text);
                  textEditingController.selection = TextSelection(
                    baseOffset: cursorPos - 1 + "$value".length + 1,
                    extentOffset: cursorPos - 1 + "$value".length + 1,
                  );
                  resetMatchTracker();
                  isShortcutTracking = false;
                  CometChatUIEvents.hidePanel(
                      composerId, CustomUIPosition.composerPreview
                  );
                })
            ));
            suggestionListEventSink?.add(list);
          }
        }
      }

      void updatePreviousText(String text) {
        if (previousTextEventSink != null) {
          previousTextEventSink!.add(text);
        }
      }

      void resetMatchTracker() {
        suggestionListEventSink?.add([]);
        if (onSearch != null) {
          onSearch!(null);
        }
      }

      @override
      TextStyle getMessageInputTextStyle(CometChatTheme theme) {
        // TODO: implement getMessageInputTextStyle
        throw UnimplementedError();
      }

      @override
      void handlePreMessageSend(BuildContext context, BaseMessage baseMessage) {
        // TODO: implement handlePreMessageSend
      }

      @override
      void onChange(TextEditingController textEditingController, String previousText) {
        var cursorPosition = textEditingController.selection.base.offset;
        if (textEditingController.text.isEmpty) {
          resetMatchTracker();
          if (previousText.length > textEditingController.text.length) {
            if (previousText[cursorPosition] == trackingCharacter && isShortcutTracking) {
              isShortcutTracking = false;
              if (onSearch != null) {
                onSearch!(null);
              }
              CometChatUIEvents.hidePanel(composerId, CustomUIPosition.composerPreview);
            }
            return;
          }
        }

        if (previousText.length > textEditingController.text.length) {
          if (previousText[cursorPosition] == trackingCharacter) {
            isShortcutTracking = false;
            if (onSearch != null) {
              onSearch!(null);
            }
            CometChatUIEvents.hidePanel(composerId, CustomUIPosition.composerPreview);
          }
        } else {
          String previousCharacter = cursorPosition == 0 ? "" : textEditingController.text[cursorPosition - 1];
          bool isSpace = cursorPosition == 1 || (textEditingController.text.length > 1 && cursorPosition > 1 && (textEditingController.text[cursorPosition - 2] == " " || textEditingController.text[cursorPosition - 2] == "\n"));

          if (isShortcutTracking) {
            isShortcutTracking = false;
            if (onSearch != null) {
              onSearch!(null);
            }
            CometChatUIEvents.hidePanel(composerId, CustomUIPosition.composerPreview);
          } else if (previousCharacter == trackingCharacter && isSpace) {
            isShortcutTracking = true;
            if (onSearch != null) {
              onSearch!(trackingCharacter);
            }
            CometChatUIEvents.showPanel(composerId, CustomUIPosition.composerPreview, (context) => getLoadingIndicator(context, cometChatTheme));
            prepareShortcuts(textEditingController);
          }
        }
      }

      @override
      void onScrollToBottom(TextEditingController textEditingController) {
        // TODO: implement onScrollToBottom
      }
    }
    ```
  </Tab>
</Tabs>

<Tabs>
  <Tab title="Dart">
    ```dart theme={null}
    import 'package:cometchat_chat_uikit/cometchat_chat_uikit.dart';
    import 'package:flutter/material.dart';
    import 'shortcut_formatter.dart';

    class ShortcutFormatterExample extends StatefulWidget {
      const ShortcutFormatterExample({super.key});

      @override
      State<ShortcutFormatterExample> createState() => _ShortcutFormatterExampleState();
    }

    class _ShortcutFormatterExampleState extends State<ShortcutFormatterExample> {

      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: SafeArea(
            child: CometChatConversationsWithMessages(
                messageConfiguration: MessageConfiguration(
                  messageComposerConfiguration: MessageComposerConfiguration(
                    textFormatters: [ShortcutFormatter()],
                  ),
                )
            ),
          ),
        );
      }
    }
    ```
  </Tab>
</Tabs>

<Tabs>
  <Tab title="Android">
    <img src="https://mintcdn.com/cometchat-013b37f0/wLjUOrqmbnHWpp4o/images/be678592-shortcut_formatter_overview_cometchat_screens-f92866b98167048feef232b253d55d77.png?fit=max&auto=format&n=wLjUOrqmbnHWpp4o&q=85&s=4a9fb78addc762450c253bd8fc3a6fce" alt="Image" width="4498" height="3121" data-path="images/be678592-shortcut_formatter_overview_cometchat_screens-f92866b98167048feef232b253d55d77.png" />
  </Tab>

  <Tab title="iOS">
    <img src="https://mintcdn.com/cometchat-013b37f0/pPsJINHbgj20OYP-/images/81c10d96-shortcut_formatter_overview_cometchat_screens-c93927de826675bc0a8e3f92c59297ab.png?fit=max&auto=format&n=pPsJINHbgj20OYP-&q=85&s=1c53ddab73942f6eaf6a37ff1b5dd596" alt="Image" width="4498" height="3121" data-path="images/81c10d96-shortcut_formatter_overview_cometchat_screens-c93927de826675bc0a8e3f92c59297ab.png" />
  </Tab>
</Tabs>

***
