aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/index.js52
-rw-r--r--src/ui/Discussion.js24
-rw-r--r--src/ui/Note.js148
-rw-r--r--src/ui/Selection.js90
-rw-r--r--src/ui/index.js2
-rw-r--r--src/ui/theme.js21
6 files changed, 274 insertions, 63 deletions
diff --git a/src/index.js b/src/index.js
index ba50149..b32054e 100644
--- a/src/index.js
+++ b/src/index.js
@@ -2,8 +2,8 @@ import 'preact/debug';
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import * as jsonld from 'jsonld';
-import { h, Component, render } from 'preact';
-import { Discussion } from './ui';
+import { h, Fragment, Component, render } from 'preact';
+import { Discussion, Selection } from './ui';
const context = [
'https://www.w3.org/ns/activitystreams',
@@ -154,6 +154,27 @@ class GraphContainer extends Component {
}
}
+class SelectionContainer extends Component {
+ state = {
+ selection: [],
+ };
+
+ toggle = (id) => {
+ const { selection } = this.state;
+ if (selection.indexOf(id) < 0)
+ this.setState({ selection: [ ...selection, id ] });
+ else
+ this.setState({ selection: selection.filter(i => i !== id) });
+ }
+
+ render() {
+ return this.props.render({
+ ...this.state,
+ toggleSelected: this.toggle,
+ });
+ }
+}
+
class CollapseContainer extends Component {
state = {};
@@ -193,17 +214,26 @@ const search = new URLSearchParams(window.location.search);
const graph = search.has('graph') ? search.get('graph') : 'lib/graph.json';
const app = (
<GraphContainer graph={graph} render={({ name, items }) => (
- <CollapseContainer
- items={items}
- render={({ collapsed, toggleCollapsed }) => (
- <Discussion
- name={name}
+ <SelectionContainer render={({ selection, toggleSelected }) => (
+ <>
+ <CollapseContainer
items={items}
- collapsed={collapsed}
- toggleCollapsed={toggleCollapsed}
+ render={({ collapsed, toggleCollapsed }) => (
+ <Discussion
+ name={name}
+ items={items}
+ collapsed={collapsed}
+ toggleCollapsed={toggleCollapsed}
+ toggleSelected={toggleSelected}
+ />
+ )}
+ />
+ <Selection
+ items={selection.map(id => items[id])}
+ toggleSelected={toggleSelected}
/>
- )}
- />
+ </>
+ )} />
)} />
);
render(app, document.body);
diff --git a/src/ui/Discussion.js b/src/ui/Discussion.js
index 2cea1c1..3176c03 100644
--- a/src/ui/Discussion.js
+++ b/src/ui/Discussion.js
@@ -6,20 +6,23 @@ import { Links } from './Links';
import { Note } from './Note';
css`
-article > div {
- position: relative;
+article {
+ display: flex;
+ flex-direction: column;
+
+ overflow: auto;
+ flex: 1 1 auto;
}
-article > div > canvas {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
+article > div {
+ position: relative;
}
`;
-export const Discussion = ({ name, items, collapsed, toggleCollapsed }) => {
+export const Discussion = ({
+ name, items, collapsed,
+ toggleCollapsed, toggleSelected,
+}) => {
const [{ width, height, positions, links }, setState] = useState({
width: 0,
height: 0,
@@ -67,7 +70,8 @@ export const Discussion = ({ name, items, collapsed, toggleCollapsed }) => {
}
collapsed={isCollapsed}
position={positions[item.id]}
- onHide={toggleCollapsed}
+ onCollapse={toggleCollapsed}
+ onSelect={toggleSelected}
onSize={setNodeSize}
/>
);
diff --git a/src/ui/Note.js b/src/ui/Note.js
index 6bdec04..404979c 100644
--- a/src/ui/Note.js
+++ b/src/ui/Note.js
@@ -17,44 +17,59 @@ css`
margin: 0;
}
`;
-const Space = () => <div class="flex-space" />;
+const Space = (props) => <div class="flex-space" {...props} />;
css`
section > header {
display: flex;
flex-direction: row;
- height: 1.75rem;
+ height: 2.5rem;
color: var(--theme-header-fg);
background: var(--theme-header-bg);
-
- cursor: pointer;
- transition: opacity 0.3s;
-}
-
-section > header:hover {
- opacity: 0.5;
}
section > header.small {
- height: 0.5em;
+ height: 0.75rem;
}
section > header > * {
- margin: 0 0.5em;
+ margin: 0 0.5rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
- line-height: 1.75rem;
+ line-height: 2.5rem;
}
-section > header > img {
- height: 1.5rem;
- width: 1.5rem;
- flex: 0 0 1.5rem;
- margin: 0.125rem;
- border-radius: 0.2rem;
+section > header > .avatar {
+ height: 2rem;
+ width: 2rem;
+ flex: 0 0 auto;
+ margin: 0.25rem;
+ border-radius: 0.3rem;
+}
+section > header > .avatar img {
+ width: inherit;
+ height: inherit;
+}
+section > header > .collapse {
+ height: 1rem;
+ width: 1rem;
+ flex: 0 0 auto;
+ margin: 0.6rem 0.5rem;
+ border-radius: 0.3rem;
+ 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 {
@@ -67,30 +82,45 @@ section > header > .time {
color: #363636;
}
`;
-const Header = ({ user, published, onClick }) => {
+const Header = ({ id, user, published, collapsed, onCollapse }) => {
const username = user.name || user.preferredUsername;
+ const onClick = onCollapse && ((e) => {
+ e.preventDefault();
+ onCollapse(id);
+ });
+
return (
- <header onClick={onClick}>
- <img src={new URL(user.icon.url, user.id)} />
- <span className="user">{username}</span>
+ <header>
+ <a class="avatar" href={user.id}>
+ <img src={new URL(user.icon.url, user.id)} />
+ </a>
+ <span class="user">{username}</span>
<Space />
- <span className="time"><Time time={published} /></span>
+ <a class="time" href={id}>
+ <Time time={published} />
+ </a>
+ <a class="collapse" href="#" onClick={onClick}>
+ {collapsed ? '+' : '-'}
+ </a>
</header>
);
};
css`
section {
- position: absolute;
- overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
- width: 22em;
+ width: 22rem;
+
+ 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.4em;
+ border-radius: 0.4rem;
}
section.type-Tombstone {
@@ -99,7 +129,9 @@ section.type-Tombstone {
}
section > main {
- padding: 1em;
+ position: relative;
+ padding: 0.75rem;
+ overflow: hidden;
font-family: serif;
}
@@ -124,43 +156,77 @@ section.collapsed {
section.collapsed > main {
display: none;
}
+
+section.ellipsis > main {
+ max-height: 7em;
+}
+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%);
+}
`;
const colors = new ColorHash({ lightness: 0.8 });
export const Note = ({
id, type, attributedTo, published, content,
- smallHeader = false, collapsed = false, position,
- onHide, onSize,
+ smallHeader = false, collapsed = false, ellipsis = false, position,
+ onCollapse, onSelect, onSize,
}) => {
const backgroundColor = attributedTo.color || colors.hex(attributedTo.id);
+ const onClick = onSelect && ((e) => {
+ e.preventDefault();
+ onSelect(id);
+ });
+
const root = useRef(null);
- useLayoutEffect(() => {
+ onSize && useLayoutEffect(() => {
onSize(id, root.current.offsetWidth, root.current.offsetHeight);
});
- const onClickHeader = (e) => {
- e.preventDefault();
- onHide(id);
- };
-
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': position ? 'visible' : 'hidden',
+ 'visibility': onSize && !position ? 'hidden' : 'visible',
}}
- class={cn(`type-${type}`, collapsed && 'collapsed')}
+ class={cn(
+ `type-${type}`,
+ collapsed && 'collapsed',
+ ellipsis && 'ellipsis',
+ )}
data-id={id}
ref={root}
>
{smallHeader
- ? <header className="small" onClick={onClickHeader} />
- : <Header onClick={onClickHeader} user={attributedTo} published={published} />
+ ? <header className="small" onClick={onCollapse} />
+ : <Header
+ id={id}
+ user={attributedTo}
+ published={published}
+ collapsed={collapsed}
+ onCollapse={onCollapse}
+ />
}
- <main innerHTML={content} />
+ <main
+ innerHTML={content}
+ onClick={onClick}
+ />
</section>
);
};
diff --git a/src/ui/Selection.js b/src/ui/Selection.js
new file mode 100644
index 0000000..d6e62ab
--- /dev/null
+++ b/src/ui/Selection.js
@@ -0,0 +1,90 @@
+import { h } from 'preact';
+import { useState, useEffect } from 'preact/hooks';
+import cn from 'classnames';
+import css from './css';
+import { Note } from './Note';
+
+css`
+.drawer {
+ background: var(--theme-note-fg);
+
+ flex: 0 0 auto;
+
+ /* wtf webkit */
+ -webkit-backface-visibility: hidden;
+}
+
+.drawer .handle {
+ height: 2rem;
+ margin-bottom: 1rem;
+}
+
+.drawer .handle button {
+ display: block;
+
+ height: 2rem;
+ line-height: 2rem;
+ margin: -1rem auto 0;
+ padding: 0.25rem;
+ background: var(--theme-note-bg);
+ border-radius: 0.4rem;
+}
+
+.drawer .scroller {
+ display: flex;
+ overflow: auto;
+ width: 100%;
+
+ transition: max-height 0.3s;
+}
+
+.drawer .contents {
+ display: flex;
+
+ align-items: flex-start;
+
+ gap: 1rem;
+ padding: 0 2rem;
+}
+
+.drawer .contents > * {
+ flex: 0 0 auto;
+}
+`;
+
+export const Drawer = ({ height, children }) => {
+ const [ hidden, setHidden ] = useState(true);
+
+ return (
+ <div class="drawer">
+ <div class="handle">
+ <button onClick={() => setHidden(!hidden)}>
+ ###
+ </button>
+ </div>
+ <div
+ class="scroller"
+ style={{
+ height,
+ 'max-height': hidden ? '0' : height,
+ }}
+ >
+ <div class="contents">
+ {children}
+ </div>
+ </div>
+ </div>
+ );
+};
+
+export const Selection = ({ items, toggleSelected }) => (
+ <Drawer height="12rem" style={{ }} >
+ {items.map((item) => (
+ <Note
+ {...item}
+ ellipsis
+ onSelect={toggleSelected}
+ />
+ ))}
+ </Drawer>
+);
diff --git a/src/ui/index.js b/src/ui/index.js
index 38b5b1b..fa831e2 100644
--- a/src/ui/index.js
+++ b/src/ui/index.js
@@ -1,6 +1,8 @@
import './theme';
import { Discussion } from './Discussion';
+import { Selection } from './Selection';
export {
Discussion,
+ Selection,
};
diff --git a/src/ui/theme.js b/src/ui/theme.js
index 372cd46..58c8397 100644
--- a/src/ui/theme.js
+++ b/src/ui/theme.js
@@ -1,11 +1,19 @@
import css from './css';
css`
+html {
+ font-size: 11px;
+ margin: 0;
+ padding: 0;
+}
+
body {
--theme-backdrop: #1e1e1e;
--theme-fg: #eeeeee;
--theme-note-bg: #eeeeee;
--theme-note-fg: #363636;
+ --theme-note-bg-trans: rgba(238,238,238,0);
+ --theme-note-fg-trans: rgba(54,54,54,0);
--theme-header-fg: #121212;
}
@@ -13,6 +21,8 @@ body.darktheme {
--theme-backdrop: #1e1e1e;
--theme-note-bg: #363636;
--theme-note-fg: #eeeeee;
+ --theme-note-bg-trans: rgba(54,54,54,0);
+ --theme-note-fg-trans: rgba(238,238,238,0);
--theme-header-fg: #121212;
}
@@ -20,7 +30,16 @@ body {
color: var(--theme-fg);
background: var(--theme-backdrop);
font-family: sans-serif;
- font-size: 11px;
+
+ margin: 0;
+ padding: 0;
+ width: 100vw;
+ height: 100vh;
+ overflow: hidden;
+
+ display: flex;
+ flex-direction: column;
+ justify-content: space-around;
}
`;