> ## Documentation Index
> Fetch the complete documentation index at: https://docs.photon.codes/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Messages

> Receive, narrow, and act on incoming messages

export const TypeTooltip = ({name, type, children}) => {
  const [visible, setVisible] = React.useState(false);
  const [pos, setPos] = React.useState({
    top: 0,
    left: 0
  });
  const triggerRef = React.useRef(null);
  const show = () => {
    if (triggerRef.current) {
      const rect = triggerRef.current.getBoundingClientRect();
      setPos({
        top: rect.bottom + 6,
        left: rect.left
      });
    }
    setVisible(true);
  };
  const hide = () => setVisible(false);
  return <>
      <span ref={triggerRef} onMouseEnter={show} onMouseLeave={hide} style={{
    cursor: "pointer",
    position: "relative",
    display: "inline"
  }}>
        {children || <code>{name}</code>}
      </span>
      {visible && <span style={{
    position: "fixed",
    top: pos.top,
    left: pos.left,
    zIndex: 9999,
    padding: "8px 12px",
    borderRadius: "8px",
    fontSize: "13px",
    lineHeight: "1.5",
    fontFamily: "'Azeret Mono', monospace",
    whiteSpace: "pre",
    backgroundColor: "var(--tw-prose-pre-bg, #1e1e1e)",
    color: "var(--tw-prose-pre-code, #e5e5e5)",
    border: "1px solid var(--border, rgba(128,128,128,0.2))",
    boxShadow: "0 4px 16px rgba(0,0,0,0.3)",
    pointerEvents: "none"
  }}>
          {type}
        </span>}
    </>;
};

Every incoming message arrives through `app.messages` as a `[Space, Message]` tuple. The space is already bound to the originating conversation — you don't need to resolve it yourself to reply.

## Receiving messages

```ts theme={null}
for await (const [space, message] of app.messages) {
  // handle the message
}
```

Messages from every configured provider merge into this single stream. The order reflects arrival time.

## The Message shape

Every message conforms to <TypeTooltip name="Message" type={`interface Message<TPlatform extends string = string, TSender extends User = User, TSpace extends Space = Space> {
content: Content;
direction: "inbound" | "outbound";
edit(newContent: ContentInput): Promise<void>;
readonly id: string;
platform: TPlatform;
react(reaction: string): Promise<(Message<TPlatform, AgentSender, TSpace> & {
    content: Reaction;
}) | undefined>;
read(): Promise<void>;
reply(content: ContentInput): Promise<Message<TPlatform, AgentSender, TSpace> | undefined>;
reply(...content: [
    ContentInput,
    ContentInput,
    ...ContentInput[]
]): Promise<Message<TPlatform, AgentSender, TSpace>[]>;
sender: TSender | undefined;
space: TSpace;
timestamp: Date;
unsend(): Promise<void>;
}`} />.

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Description</th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td><code>id</code></td>
      <td>Platform-assigned message identifier.</td>
    </tr>

    <tr>
      <td><code>content</code></td>
      <td>Discriminated union on <code>type</code> — see <a href="#narrowing-content">Narrowing content</a> for the full set of variants.</td>
    </tr>

    <tr>
      <td><code>sender</code></td>
      <td>The <TypeTooltip name="User" type={`interface User {
            readonly __platform: string;
            readonly id: string;
            readonly kind?: "agent";
          }`} /> who sent the message.</td>
    </tr>

    <tr>
      <td><code>space</code></td>
      <td>The <TypeTooltip name="Space" type={`interface Space<_Def = unknown> {
            readonly __platform: string;
            avatar(input: string | URL, options?: {
                mimeType?: string;
            }): Promise<void>;
            avatar(input: Buffer, options: {
                mimeType: string;
            }): Promise<void>;
            edit(message: Message | undefined, newContent: ContentInput): Promise<void>;
            getMessage(id: string): Promise<Message | undefined>;
            readonly id: string;
            read(message: Message): Promise<void>;
            rename(displayName: string): Promise<void>;
            responding<T>(fn: () => T | Promise<T>): Promise<T>;
            send(content: ReactionBuilder): Promise<(Message<string, AgentSender> & {
                content: Reaction;
            }) | undefined>;
            send(content: ContentInput): Promise<Message<string, AgentSender> | undefined>;
            send(...content: [
                ContentInput,
                ContentInput,
                ...ContentInput[]
            ]): Promise<Message<string, AgentSender>[]>;
            startTyping(): Promise<void>;
            stopTyping(): Promise<void>;
            unsend(message: Message | undefined): Promise<void>;
          }`} /> the message was sent into.</td>
    </tr>

    <tr>
      <td><code>platform</code></td>
      <td>Name of the provider that delivered the message (e.g. <code>"iMessage"</code>, <code>"terminal"</code>).</td>
    </tr>

    <tr>
      <td><code>timestamp</code></td>
      <td><code>Date</code> of when the message was sent.</td>
    </tr>

    <tr>
      <td><code>react(reaction)</code></td>
      <td>React to this message. Returns the reaction <code>Message</code> — keep it as the handle to <code>unsend()</code> later. No-op on platforms that don't support reactions.</td>
    </tr>

    <tr>
      <td><code>reply(...content)</code></td>
      <td>Reply threaded to this message. Falls back silently on platforms without thread support.</td>
    </tr>

    <tr>
      <td><code>edit(newContent)</code></td>
      <td>Rewrite the content of this outbound message. Fire-and-forget.</td>
    </tr>

    <tr>
      <td><code>unsend()</code></td>
      <td>Retract this outbound message. Fire-and-forget.</td>
    </tr>
  </tbody>
</table>

## Narrowing content

`Content` is a discriminated union. Narrow on `message.content.type` before accessing fields:

```ts theme={null}
for await (const [space, message] of app.messages) {
  switch (message.content.type) {
    case "text":
      console.log(message.content.text);
      break;
    case "attachment":
      console.log(
        `${message.content.name} (${message.content.mimeType})`,
        await message.content.read(),
      );
      break;
    case "voice":
      console.log(`voice note (${message.content.duration}s)`);
      break;
    case "contact":
      console.log(message.content.name?.formatted, message.content.phones);
      break;
    case "richlink":
      console.log(message.content.url, await message.content.title());
      break;
    case "reaction":
      console.log(`${message.content.emoji} on ${message.content.target.id}`);
      break;
    case "poll":
      console.log(message.content.title, message.content.options);
      break;
    case "poll_option":
      console.log(`vote ${message.content.selected ? "+" : "-"}`, message.content.title);
      break;
    case "group":
      console.log(`group of ${message.content.items.length} items`);
      break;
    case "custom":
      console.log(message.content.raw);
      break;
  }
}
```

<Accordion title="Content" description="The incoming content variants that every Message carries. Most platforms only emit a subset — narrow defensively.">
  | Type            | Fields                                                                                                |
  | --------------- | ----------------------------------------------------------------------------------------------------- |
  | `"text"`        | `text: string`                                                                                        |
  | `"markdown"`    | `markdown: string` — outbound-only styled text                                                        |
  | `"attachment"`  | `id: string`, `name: string`, `mimeType: string`, `size?: number`, `read()`, `stream()`               |
  | `"voice"`       | `name?: string`, `mimeType: string`, `duration?: number`, `size?: number`, `read()`, `stream()`       |
  | `"contact"`     | `name?`, `phones?`, `emails?`, `addresses?`, `org?`, `urls?`, `birthday?`, `note?`, `photo?`, `user?` |
  | `"richlink"`    | `url: string`, `title()`, `summary()`, `cover()`                                                      |
  | `"reaction"`    | `emoji: string`, `target: Message`                                                                    |
  | `"poll"`        | `title: string`, `options: { title: string }[]`                                                       |
  | `"poll_option"` | `option: { title }`, `poll: Poll`, `selected: boolean`, `title: string` — sent as a vote              |
  | `"group"`       | `items: Message[]` — bundled multi-message unit                                                       |
  | `"reply"`       | `content: Content`, `target: Message` — threaded reply wrapping inner content                         |
  | `"edit"`        | `content: Content`, `target: Message` — rewrite of a previously-sent message                          |
  | `"unsend"`      | `target: Message` — retraction of a previously-sent message                                           |
  | `"typing"`      | `state: "start" \| "stop"` — typing indicator signal                                                  |
  | `"streamText"`  | `stream: () => AsyncIterable<string>` — streaming text content                                        |
  | `"custom"`      | `raw: unknown` — platform-specific structured data                                                    |
</Accordion>

Outgoing-only variants like `"effect"` (an iMessage screen effect wrapping inner content) appear on messages you sent and are echoed by the platform; see [iMessage](/spectrum-ts/providers/imessage) for the builder.

## Filtering out your own messages

On platforms where your account also receives its own sends, guard with a platform-specific check — for example, iMessage carries an `isFromMe` flag on the raw message extra you can expose through a provider `message.schema`.

For unified logic, compare the sender to a known identity:

```ts theme={null}
for await (const [space, message] of app.messages) {
  if (message.sender.id === myAccountId) continue;
  // handle incoming
}
```

## Acting on a message

Every message is its own context. You can reply, react, or send new content into the space:

```ts theme={null}
for await (const [space, message] of app.messages) {
  await message.react("love");              // react
  await message.reply("Got it");            // threaded reply
  await space.send("Here's more context");  // fresh send into the space
}
```

See [Content](/spectrum-ts/content) for all the ways to build outgoing messages, and [Reactions and replies](/spectrum-ts/reactions-and-replies) for the details of `react` and `reply`.
