# Comments

A comment thread (layouts#2; shape of hybriel's demo-social-network): **"1 comment" / "n comments"**, then every comment — an
avatar (the initial of the name), a bubble with the **name**, the **time**, **"edited"** (the earlier versions fold out) and the
**text as formatted Markdown**; under it a **like** (♥ and the count) and, for the comment's author, **Edit / Delete** (Delete asks
first) — and the **form below**, typed in the Markdown editor. Live demo: https://components.hybriel.worldapi.org/comments

## Files
Copy `components/comments/` (`comments.hl`) **and** `components/md-editor/` (`md-editor.hl`, mission 006 or later — it needs its
`readonly` attribute): `comments.hl` imports `../md-editor/md-editor.hl`. Both carry their own `Style`; every colour is a semantic
token with a black/white/grey fallback (root README "Look").

## Use it in an app
The block is generic: no app data, no storage. **The host passes the comments and the current user, hears what the user did through
four functions it hands (hybriel#87), decides what is stored, and gives a new list back** — the block shows the list it is given.

```
import Comments from './comments/comments.hl'

Hybrid thread = commentsOf(ticketId, me)      // the app's own records, in the shape below
String me = session != null ? session.user.name : ''

posted = (text) => { thread = emit server commentPost(ticketId, text) }        // the face stores it and answers the new list
edited = (id, text) => { thread = emit server commentEdit(ticketId, id, text) }
deleted = (id) => { thread = emit server commentDelete(ticketId, id) }
liked = (id) => { thread = emit server commentLike(ticketId, id) }

View { Comments { comments = thread  user = me  onPost = posted  onEdit = edited  onDelete = deleted  onLike = liked } }
```

### Data in
| Member | Meaning |
|---|---|
| `comments` | the list, oldest first: `{ id, author, when, text, likes, liked, history }` (below) |
| `user` | the current user's name (compared with `author`); `''` = nobody: no form, no Edit / Delete, the like does nothing |

A comment: `id` (any text or number, unique in the list — the rows are keyed by it), `author` (the name shown, and who may edit),
`when` (the time as the app writes it, e.g. "Oct 17, 09:12"), `text` (Markdown), `likes` (a number), `liked` (`true` when `user`
likes it), `history` (the earlier versions, oldest first: `[{ when, text }]`; empty or missing = never edited). The app checks
the rights again on the server — the block only hides what the user may not do.

### Data out — the four functions
| Function | Called when | With |
|---|---|---|
| `onPost(text)` | the form is sent (the Comment button, or Ctrl+Enter in the editor); empty text sends nothing | the Markdown typed |
| `onEdit(id, text)` | Save of an edit with a changed, non-empty text | the comment's id, the new Markdown |
| `onDelete(id)` | Delete, then Delete again in "Delete this comment?" (Keep takes it back) | the comment's id |
| `onLike(id)` | the like is pressed (only with a `user`) | the comment's id — the host switches `liked` and the count |

With `attach = "true"` (below) it is `onPost(text, files)`: `files` are the browser Files waiting in the form (a post may be files only).
`onPost` may answer `false` to keep the text in the editor (e.g. the server refused it); anything else empties the form. Edit / Delete
show only where `author == user`.

### Attachments (`attach = "true"`, optional)
Without `attach` nothing changes (`onPost(text)`, no files). With it the form sits inside the shared FilesUpload (pick mode, imported
from `../files-upload/files-upload.hl` — copy that folder too): choose / camera / drop / paste files, they wait as thumbnails, and
`onPost(text, files)` gets the text and the browser Files; the host stores them. Answering `false` keeps text and files. Members:
`maxFileSize` (bytes per file), `accept`, `attachText`, `chooseText`, `cameraText`, `filesLabel`. The editor in the form has
`files = "off"` (no File button; pasted / dropped files go to the FilesUpload).

**Files per comment:** a comment record may carry `files: [{ name, url, image, size }]`. Pictures (`image`) show as 6 rem thumbnails
(`a.comment-thumb`, opens the full picture in a new tab); other files are rows (`a.comment-file`: kind, name, size). A file without
`url` shows as a row without link (`span.comment-file`) — e.g. for users who may not fetch it. Sizes are written by integer maths.

### A state change in the form's footer (`states`, optional)
For a host whose comments can move something between states (the tickets app): `states = [{ value, label }]` — the choices, the current
one left out — puts **`State [select]`** in front of the post button (the right side of the form's footer). The select starts on
"no change" (`keepText`). The button says what the post does: **"Comment"** (text or files, no state), **"Set to review"** (a state,
nothing to say; `stateOnlyLabel`), **"Comment and set to review"** (both; `bothLabel`). With `states` the host hears
`onPost(text, files, state)`: `state` is `''` when unchanged, and a post of only a state is sent too. The host decides what to do with the
three (the tickets page posts the comment first, then sets the state). Members: `states`, `stateLabel` ("State"), `keepText`
("no change"), `stateOnlyLabel` ("Set to"), `bothLabel` ("Comment and set to").

### Texts
`oneLabel` ("comment"), `manyLabel` ("comments"), `emptyText` ("No comments yet."), `placeholder` ("Write a comment…"), `postLabel`
("Comment"), `editLabel`, `deleteLabel`, `saveLabel`, `cancelLabel`, `askText` ("Delete this comment?"), `keepLabel`, `editedLabel`
("edited"), `likeLabel` (the like's title), `signInText` (shown instead of the form when there is no user; empty = nothing).

## How it is built
- Every text is drawn by MdEditor's own renderer: `MdEditor { value = text  readonly = "true" }` — the same Markdown subset the form
  makes, so a comment looks as it was typed. MdEditor builds its content in the browser once per element; hl:web builds a composed
  child anew when a value bound into it changes, so a changed text — an edit here, or a new list the host got from a push — is drawn
  anew (gate: "bob edits his comment elsewhere"), and a deleted comment takes its row with it (the rows are keyed by `id`).
- The text is drawn in the browser: the server's HTML carries each comment's Markdown in the editor's hidden field, not formatted.
- The form is an MdEditor in a one-row list too: after a post its key changes and an empty editor takes its place.
- Which comment is being edited / asked about is one member each (`editingId`, `askingId`): **one block per page** (a composed
  component's members are the page's state).
- "edited" is a `<details>` with the earlier versions as their Markdown source (oldest first, "Edited once" / "Edited n times"). It looks like a quiet
  label beside the time (dotted underline, muted); open, the versions run in under the meta line, muted, unframed — the block switches off an app
  theme's framed foldable (border, header strip, margins) for it.
- Pure Hybriel: no `.js`, no network calls of its own.

## Markup
`<comments-block>`: `h2.comments-count`, `p.comments-empty`, `ol.comment-list` > `li.comment[data-id]` > `<comment-avatar>` +
`<comment-main>` (`<comment-bubble>` with `<comment-meta>` — `strong.comment-author`, `time`, `details.comment-history` — and the
text `md-editor[data-readonly="true"]` or `form.comment-edit`; `<comment-actions>` with `button.comment-like[aria-pressed]`,
`button.comment-edit-start`, `button.comment-delete`, `<comment-ask>`), then `form.comment-form` (MdEditor + `button.comment-post`)
or `p.comments-sign-in`.
