aboutsummaryrefslogtreecommitdiffstats
path: root/client/src/ui/Note.js
diff options
context:
space:
mode:
authors-ol <s+removethis@s-ol.nu>2022-01-13 17:29:38 +0000
committers-ol <s+removethis@s-ol.nu>2022-01-13 17:29:38 +0000
commit31159ddff5bc489ab6216a72304e1f7c7ce4a432 (patch)
treee0c6efe46d4590c77ceabb4094c8ecd7c4289e00 /client/src/ui/Note.js
parentmove everything into server/ (diff)
parentmove everything into client/ (diff)
downloadfedidag-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.js249
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>
+ );
+};