diff --git a/ui-kit/ios/compact-message-composer.mdx b/ui-kit/ios/compact-message-composer.mdx index 464bcb5d9..40ec390f6 100644 --- a/ui-kit/ios/compact-message-composer.mdx +++ b/ui-kit/ios/compact-message-composer.mdx @@ -351,6 +351,140 @@ The following properties are exposed by `RichTextToolbarStyle`: --- +### Trailing toolbar actions + +`set(richTextToolbarActions:)` appends your own buttons at the trailing end of the built-in toolbar — after the format buttons, separated by a divider — without replacing the toolbar. It takes the same closure shape as `set(attachmentOptions:)`, receiving the current `User?` and `Group?`, and returns a list of `CometChatRichTextToolbarAction` items. + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `id` | `String` | Identifies the action, and looks the rendered button up in `CometChatRichTextToolbar.trailingActionButtons`. | +| `icon` | `UIImage?` | The button glyph. Rendered as-is — apply `.withRenderingMode(.alwaysTemplate)` for `tint` or the toolbar's `iconTintColor` to take effect. | +| `onClick` | `(CometChatComposerInput) -> Void` | Called on tap with a live handle onto the composer's input. | +| `tint` | `UIColor?` | Overrides `RichTextToolbarStyle.iconTintColor` when non-nil. | +| `accessibilityLabel` | `String?` | VoiceOver label. Falls back to `id`, so a button is never unlabelled. | + +Each action's `onClick` receives a **`CometChatComposerInput`** — a live handle for reading and mutating the draft, so the composer stays the owner of the field: + +| Member | Description | +| ------ | ----------- | +| `text: String` | The input's contents without attributes. | +| `attributedText: NSAttributedString` | The contents, including formatting attributes. | +| `selectedRange: NSRange` | The current selection, or a zero-length range at the caret. | +| `hasSelection: Bool` | Whether text is selected, as opposed to a caret being placed. | +| `typingAttributes: [NSAttributedString.Key: Any]` | Attributes the next typed character inherits — set this to format text typed next rather than a selection. | +| `mentionRanges: [NSRange]` | The ranges currently occupied by mentions. | +| `attributes(at:)` | The attributes at a location, or empty if out of bounds. | +| `insertAtCaret(_:)` | Insert at the caret, replacing any selection; the caret lands after the inserted text. | +| `setAttributedText(_:preservingSelection:)` | Replace the contents, keeping the selection by default. | +| `setSelectedRange(_:)` | Move the caret or change the selection, clamped to the current length. | +| `applyAttributes(_:to:protectingKitRuns:)` / `removeAttributes(_:from:protectingKitRuns:)` | Add or strip attributes over a range. | +| `applyAttributesToSelection(_:protectingKitRuns:)` / `removeAttributesFromSelection(_:protectingKitRuns:)` | The same over the current selection; each returns whether there was a selection to act on. | +| `commit()` | Resync the toolbar and send button partway through a long edit. | + + + +```swift +compactComposer.set(richTextToolbarActions: { user, group in + [ + CometChatRichTextToolbarAction( + id: "insert_greeting", + icon: UIImage(systemName: "hand.wave")?.withRenderingMode(.alwaysTemplate), + accessibilityLabel: "Greeting" + ) { input in + input.insertAtCaret(NSAttributedString(string: "Hello! ")) + } + ] +}) +``` + + + +Actions are registered per composer instance, so a thread composer can offer a different set from the main one. + + + +The trailing section lives **inside** the rich-text toolbar — it is not rendered when the toolbar is hidden (`showRichTextFormattingOptions = false`) or rich text is disabled (`enableRichTextFormatting = false`). + +The `CometChatComposerInput` is valid only for the duration of the `onClick` call; don't retain it. The action itself is retained by the toolbar, so capture `self` weakly. + +The composer serializes to markdown on send. The built-in formats survive, and so does text colour — see [Colour that survives send](#colour-that-survives-send). Any other attribute you apply is composer-local: markdown has no representation for it, so it is dropped from the sent message. Use those for affordances that are meant to be transient, such as marking a range while the action's own picker or sheet is open. + + + +Mutations apply immediately, but composer state that derives from the text — the toolbar's active formats and the send button — resyncs once when `onClick` returns rather than per mutation, so a multi-step edit resyncs once. Call `commit()` to resync earlier. + +#### Leaving Kit-owned runs alone + +An action that restyles a range will happily run over a mention or a link, which the Kit repaints on its own schedule — so your attribute is reverted on the next repaint anyway, and removing one of the Kit's attributes deletes it rather than restoring the Kit's value. + +The mutating methods therefore take `protectingKitRuns`, which defaults to `true`. With it on, the following are carved out of the target range and left untouched: + +- mentions +- links, including a URL still being typed +- inline code and code blocks, and any monospaced run +- fully transparent runs, so an action cannot repaint hidden text into view + +Pass `protectingKitRuns: false` to opt out and write over the whole range. To make a narrower decision yourself, read `mentionRanges` — the ranges are read at call time, so re-read them after a mutation rather than caching, as they shift with the text. Note that `mentionRanges` covers mentions only; the other carve-outs above have no public accessor. + + + +```swift +CometChatRichTextToolbarAction( + id: "red_text", + icon: UIImage(systemName: "paintpalette")?.withRenderingMode(.alwaysTemplate), + accessibilityLabel: "Red text" +) { input in + // Colouring applies to a selection; with only a caret there is nothing + // to restyle, so leave the text alone rather than guessing at a range. + guard input.hasSelection else { return } + + // Kit-owned runs are protected by default and keep their own styling. + input.applyAttributesToSelection([ + .foregroundColor: UIColor.systemRed, + RichTextFormatterManager.textColorKey: UIColor.systemRed + ]) +} +``` + + + +#### Colour that survives send + +Text colour is the one value-carrying style with a wire representation: the composer serializes it as `…`, and the message bubble parses it back. Colour therefore reaches the recipient, unlike other attributes you might apply. + +Setting `.foregroundColor` on its own is not enough. That key is written by six different things — mentions, links, inline code, code blocks, ordinary text and the user — so it records nothing about *who* set it. `RichTextFormatterManager.textColorKey` is the marker that identifies a run the user deliberately coloured, and it is what the serializer looks for. Always write and remove the two together. + + + +```swift +// Applies colour, and marks the run so it survives send. +input.applyAttributesToSelection([ + .foregroundColor: UIColor.systemRed, + RichTextFormatterManager.textColorKey: UIColor.systemRed +]) + +// Clearing colour removes both keys; the text falls back to the +// composer's own colour rather than to a value hard-coded here. +input.removeAttributesFromSelection([ + .foregroundColor, + RichTextFormatterManager.textColorKey +]) +``` + + + + + +Use opaque colours. Only `#rgb` and `#rrggbb` are valid on the wire — eight-digit `#rrggbbaa` is rejected deliberately, since a fully transparent run would be an invisible message. + +A translucent `UIColor` is **not** rejected locally: it renders in the composer, then is dropped on send, because a colour with alpha below 1 has no wire representation. That is the silent loss this section exists to prevent, so pass a fully opaque colour. + + + +The UI Kit ships no colour button of its own: the wire format and the serializer are provided, the button is yours. That is what the trailing-actions slot is for. + +--- + ## Advanced For advanced-level customization, you can set custom views to the component. This lets you tailor each aspect of the component to fit your exact needs and application aesthetics. You can create and define your views, layouts, and UI elements and then incorporate those into the component.