diff options
| author | s-ol <s+removethis@s-ol.nu> | 2022-01-13 17:29:38 +0000 |
|---|---|---|
| committer | s-ol <s+removethis@s-ol.nu> | 2022-01-13 17:29:38 +0000 |
| commit | 31159ddff5bc489ab6216a72304e1f7c7ce4a432 (patch) | |
| tree | e0c6efe46d4590c77ceabb4094c8ecd7c4289e00 /client/src/ui/Note.js | |
| parent | move everything into server/ (diff) | |
| parent | move everything into client/ (diff) | |
| download | fedidag-31159ddff5bc489ab6216a72304e1f7c7ce4a432.tar.gz fedidag-31159ddff5bc489ab6216a72304e1f7c7ce4a432.zip | |
Merge server and client repositories
Diffstat (limited to 'client/src/ui/Note.js')
| -rw-r--r-- | client/src/ui/Note.js | 249 |
1 files changed, 249 insertions, 0 deletions
diff --git a/client/src/ui/Note.js b/client/src/ui/Note.js new file mode 100644 index 0000000..16b31cb --- /dev/null +++ b/client/src/ui/Note.js @@ -0,0 +1,249 @@ +import { h } from 'preact'; +import { useRef, useLayoutEffect } from 'preact/hooks'; +import ColorHash from 'color-hash'; +import { formatRelative } from 'date-fns'; +import cn from 'classnames'; +import css from './css'; +import { Attachment } from './Attachment'; + +const now = new Date(); +const Time = ({ time }) => { + const dt = new Date(time); + return formatRelative(dt, now); +}; + +css` +.flex-space { + flex: 1; + margin: 0; +} +`; +const Space = (props) => <div class="flex-space" {...props} />; + +css` +section > header { + display: flex; + flex-direction: row; + + height: 1.75rem; + + color: var(--theme-header-fg); + background: var(--theme-header-bg); +} + +section > header.small { + height: 0.5rem; +} + +section > header > * { + margin: 0 0.35rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + line-height: 1.75rem; +} + +section > header > .avatar { + height: 1.25rem; + width: 1.25rem; + flex: 0 0 auto; + margin: 0.25rem; + border-radius: 0.2rem; + + font-size: 1.25rem; + font-weight: bold; + line-height: 1.25rem; + text-align: center; + text-decoration: none; + background: white; + color: black; +} +section > header > .avatar img { + width: inherit; + height: inherit; +} +section > header > .collapse { + height: 1rem; + width: 1rem; + flex: 0 0 auto; + margin: 0.3rem; + border-radius: 0.2rem; + border: 0.05rem solid var(--theme-header-fg); + + line-height: 0.8rem; + text-align: center; + text-decoration: none; + text-weight: bold; + + transition: background 0.3s; +} +section > header > .collapse:hover { + background: var(--theme-note-bg); +} + +section > header > .user { + flex: 0 0.5 auto; +} + +section > header > .time { + flex: 0 0 auto; + font-size: 0.8em; + color: #363636; +} +`; +const Header = ({ id, user, published, collapsed, onCollapse }) => { + const username = user.name || user.preferredUsername; + + const onClick = onCollapse && ((e) => { + e.preventDefault(); + onCollapse(id); + }); + + return ( + <header> + <a class="avatar" href={user.id}> + {user.icon + ? <img src={new URL(user.icon.url, user.id)} /> + : "?"} + </a> + <span class="user">{username}</span> + <Space /> + <a class="time" href={id}> + <Time time={published} /> + </a> + <a class="collapse" href="#" onClick={onClick}> + {collapsed ? '+' : '-'} + </a> + </header> + ); +}; + +css` +section { + display: flex; + flex-direction: column; + justify-content: space-between; + + width: 15rem; + + overflow: hidden; + color: var(--theme-note-fg); + background: var(--theme-note-bg); + box-shadow: rgba(0,0,0, 0.7) 2px 4px 5px; + + border-radius: 0.3rem; +} + +section.type-Tombstone { + color: var(--theme-note-bg); + background: var(--theme-note-fg); +} + +section > main { + position: relative; + padding: 0.5rem; + overflow: hidden; + font-family: serif; +} + +section > main > p:first-child { + margin-top: 0; +} +section > main > p:last-child { + margin-bottom: 0; +} + +section.tombstone > main { + font-family: inherit; +} + +section.hidden { + display: none; +} + +section.ellipsis > main { + max-height: 7em; +} +section.ellipsis > .attachment { + display: none; +} +section.ellipsis > main::after { + position: absolute; + display: block; + content: ''; + + pointer-events: none; + + left: 0; + right: 0; + bottom: 0; + height: 1.25em; + background: linear-gradient(0deg, var(--theme-note-bg) 5%, var(--theme-note-bg-trans) 100%); +} +section.type-Tombstone.ellipsis > main::after { + background: linear-gradient(0deg, var(--theme-note-fg) 5%, var(--theme-note-fg-trans) 100%); +} + +section.collapsed > main { + max-height: 2em; +} +`; + +const colors = new ColorHash({ lightness: 0.8 }); +export const Note = ({ + id, type, attributedTo, published, content, attachment, + smallHeader = false, collapsed = false, ellipsis = false, position, + onCollapse, onSelect, onSize, +}) => { + ellipsis ||= collapsed; + attachment ||= []; + if (!Array.isArray(attachment)) + attachment = [attachment]; + + const backgroundColor = attributedTo.color || colors.hex(attributedTo.id); + + const onClick = onSelect && ((e) => { + e.preventDefault(); + onSelect(id); + }); + + const root = useRef(null); + onSize && useLayoutEffect(() => { + onSize(id, root.current.offsetWidth, root.current.offsetHeight); + }); + + return ( + <section + style={{ + '--theme-header-bg': backgroundColor, + 'position': position && 'absolute', + 'left': position && `${position[0] - root.current.offsetWidth / 2}px`, + 'top': position && `${position[1] - root.current.offsetHeight / 2}px`, + 'visibility': onSize && !position ? 'hidden' : 'visible', + }} + class={cn( + `type-${type}`, + collapsed && 'collapsed', + ellipsis && 'ellipsis', + )} + data-id={id} + ref={root} + > + {smallHeader + ? <header className="small" onClick={onCollapse} /> + : <Header + id={id} + user={attributedTo} + published={published} + collapsed={collapsed} + onCollapse={onCollapse} + /> + } + <main + innerHTML={content} + onClick={onClick} + /> + {attachment.map(a => <Attachment {...a} />)} + </section> + ); +}; |
