Skip to content

Commit

Permalink
feat(examples): add mastodon-feed example
Browse files Browse the repository at this point in the history
  • Loading branch information
postspectacular committed Sep 15, 2023
1 parent 90a7f0b commit f6caccd
Show file tree
Hide file tree
Showing 14 changed files with 782 additions and 0 deletions.
Binary file added assets/examples/mastodon-feed.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions examples/mastodon-feed/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# mastodon-feed

![screenshot](https://raw.githubusercontent.com/thi-ng/umbrella/develop/assets/examples/mastodon-feed.png)

[Live demo](http://demo.thi.ng/umbrella/mastodon-feed/)

Please refer to the [example build instructions](https://github.com/thi-ng/umbrella/wiki/Example-build-instructions) on the wiki.

## Authors

- Karsten Schmidt

## License

© 2023 Karsten Schmidt // Apache Software License 2.0
43 changes: 43 additions & 0 deletions examples/mastodon-feed/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link
rel="icon"
href='data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">⛱️</text></svg>'
/>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>mastodon-feed · @thi.ng/umbrella</title>
<link
href="https://unpkg.com/tachyons@4/css/tachyons.min.css"
rel="stylesheet"
/>
<style>
@keyframes fadein {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}

.fadein {
animation: fadein 1s;
}
</style>
</head>
<body class="bg-black moon-gray sans-serif">
<div id="app"></div>
<a
class="db w-50 mv4 link white center tc"
href="https://github.com/thi-ng/umbrella/tree/develop/examples/mastodon-feed"
>Source code</a
>
<script type="module" src="/src/index.ts"></script>
</body>
</html>
47 changes: 47 additions & 0 deletions examples/mastodon-feed/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "@example/mastodon-feed",
"version": "0.0.1",
"private": true,
"description": "Mastodon API feed reader with support for different media types, fullscreen media modal, HTML rewriting",
"repository": "https://github.com/thi-ng/umbrella",
"author": "Karsten Schmidt <k+npm@thi.ng>",
"license": "Apache-2.0",
"scripts": {
"start": "vite --host --open",
"build": "tsc && vite build --base='./'",
"preview": "vite preview --host --open"
},
"devDependencies": {
"typescript": "^5.2.2",
"vite": "^4.4.9"
},
"dependencies": {
"@thi.ng/api": "workspace:^",
"@thi.ng/bench": "workspace:^",
"@thi.ng/checks": "workspace:^",
"@thi.ng/date": "workspace:^",
"@thi.ng/defmulti": "workspace:^",
"@thi.ng/hiccup-html": "workspace:^",
"@thi.ng/parse": "workspace:^",
"@thi.ng/rdom": "workspace:^",
"@thi.ng/rstream": "workspace:^",
"@thi.ng/strings": "workspace:^",
"@thi.ng/transducers": "workspace:^"
},
"browser": {
"process": false
},
"thi.ng": {
"readme": [
"date",
"defmulti",
"hiccup-html",
"parse",
"rdom",
"rstream",
"strings",
"transducers"
],
"screenshot": "examples/mastodon-feed.jpg"
}
}
39 changes: 39 additions & 0 deletions examples/mastodon-feed/src/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { ComponentLike } from "@thi.ng/rdom";

export interface Account {
id: string;
username: string;
name: string;
url: string;
bio: ComponentLike[];
avatar: string;
header: string;
}

export interface Message {
id: string;
url: string;
date: number;
tags: string[];
content: ComponentLike[];
media: MediaItem[];
sensitive: boolean;
reblog: boolean;
likes: number;
boosts: number;
replies: number;
}

export interface MediaItem {
id: string;
type: string;
url: string;
preview: string;
description: string;
width: number;
height: number;
}

export const LINK_COLOR = "white";
export const TAG_COLOR = "gold";
export const BUTTON_COLOR = "bg-gold.black";
73 changes: 73 additions & 0 deletions examples/mastodon-feed/src/components/account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { anchor, div, h1, img, inputText, span } from "@thi.ng/hiccup-html";
import { $input, $inputTrigger } from "@thi.ng/rdom";
import type { ISubscriber, ISubscription } from "@thi.ng/rstream";
import { BUTTON_COLOR, type Account } from "../api.js";

/**
* Form component to allow user entering a Mastodon user ID for loading &
* displaying their messages.
*
* @param value
* @param trigger
*/
export const accountChooser = (
value: ISubscription<string, string>,
trigger: ISubscriber<boolean>
) =>
div(
".pa3",
{},
inputText(".db.w-100.pa2.br2.ba.b--white-10.bg-near-black.white", {
value,
onchange: $input(value),
onkeydown: (e) => {
if (e.key === "Enter") {
$input(value)(e);
trigger.next(true);
}
},
placeholder: "User ID",
}),
anchor(
`.db.w-100.mt2.pa2.br2.${BUTTON_COLOR}.link.tc.pointer`,
{ onclick: $inputTrigger(trigger) },
"Load messages"
)
);

/**
* User account header/info component, displaying various details & images from
* the provided user account.
*
* @param account
*/
export const accountInfo = ({ username, name, bio, avatar, header }: Account) =>
div(
{},
div(
".relative",
{ style: { "padding-bottom": "33.33%" } },
div(".aspect-ratio--object.cover", {
style: { background: `url(${header}) center` },
}),
div(".absolute.z-999.bottom-0.w-100.h4", {
style: {
background:
"linear-gradient(to bottom, #0000 0%, #000e 100%)",
},
}),
img(".absolute.left-2.w-25.br-100.ba.bw3.b--black.z-999", {
src: avatar,
style: {
bottom: "-25%",
},
})
),
div(
".mt5.ml4.mt4-ns.ml7-ns.lh-solid",
{},
h1(".mv0", {}, name),
span(".f3", {}, `@${username}`)
),
div(".mt0.mb4.mh3.ph3", {}, ...bio)
);
124 changes: 124 additions & 0 deletions examples/mastodon-feed/src/components/media.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import type { Nullable } from "@thi.ng/api";
import { DEFAULT, defmulti } from "@thi.ng/defmulti";
import { div, img, video } from "@thi.ng/hiccup-html";
import { $replace } from "@thi.ng/rdom";
import type { ISubscriber, ISubscription } from "@thi.ng/rstream";
import type { MediaItem } from "../api.js";

// CSS attribs for media preview wrapper
const GRID_LAYOUT = {
style: {
display: "grid",
"grid-template-columns": "1fr 1fr",
gap: "1rem",
},
};

/**
* Wrapper component for all media previews. Creates a {@link mediaItem}
* component for each item.
*
* @param items
* @param selection
*/
export const mediaAttachments = (
items: MediaItem[],
selection: ISubscriber<Nullable<MediaItem>>
) =>
div(
".mb3",
items[0]?.type === "video" ? {} : GRID_LAYOUT,
...items.map((x) => mediaItem(x, selection))
);

/**
* Polymorphic component function, dynamically choosing an implementation based
* on media type.
*
* @remarks
* The 2nd argument is a target stream of media items to trigger the
* {@link mediaModal} overlay (currently only used for images).
*/
const mediaItem = defmulti<MediaItem, ISubscriber<Nullable<MediaItem>>, any>(
(x) => x.type,
{},
{
[DEFAULT]: ({ type }: MediaItem) =>
div(".w-100", {}, `Unsupported media type: ${type}`),

image: (item, selection) =>
div(
".relative.aspect-ratio--1x1",
{},
div(".aspect-ratio--object.cover.pointer", {
title: item.description,
style: { background: `url(${item.preview}) center` },
onclick: () => selection.next(item),
})
),

gifv: ({ description, url }) =>
div(
".relative.aspect-ratio--1x1",
{},
div(
".aspect-ratio--object.overflow-hidden",
{},
video(".w-100", {
src: url,
title: description,
preload: "auto",
playsinline: true,
autoplay: true,
muted: true,
loop: true,
})
)
),

video: ({ description, url, preview }) =>
video(".w-100", {
src: url,
title: description,
controls: true,
poster: preview,
}),
}
);

/**
* Fullscreen media modal overlay which subscribes to given stream of media
* items. Only enables the modal for incoming non-null values (currently only
* supports images).
*
* @param sel
*/
export const mediaModal = (
sel: ISubscription<Nullable<MediaItem>, Nullable<MediaItem>>
) =>
$replace(
sel.map((item) =>
item
? div(
".fadein.fixed.top-0.left-0.z-9999.w-100.vh-100.bg-black-90.flex.items-center.pointer",
{
// disable modal on click
onclick: () => sel.next(null),
},
img(".center", {
src: item.url,
style: {
"max-width": "100%",
"max-height": "100%",
},
}),
div(
".fixed.bottom-1.left-1.pv2.ph3.mw7-l.br2.bg-black-80",
{},
item.description || "😢 No alt text provided..."
)
)
: // invisible div if no item
div(".dn", {})
)
);
32 changes: 32 additions & 0 deletions examples/mastodon-feed/src/components/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Nullable } from "@thi.ng/api";
import { defFormat } from "@thi.ng/date";
import { anchor, div } from "@thi.ng/hiccup-html";
import type { ISubscriber } from "@thi.ng/rstream";
import { unitless } from "@thi.ng/strings";
import type { MediaItem, Message } from "../api.js";
import { mediaAttachments } from "./media.js";

const fmt = defFormat(["dd", " ", "MMM", " ", "yyyy", " ", "HH", ":", "mm"]);

export const message = (
{ url, content, media, replies, likes, boosts, date }: Message,
mediaSel: ISubscriber<Nullable<MediaItem>>
) =>
div(
".ma3.pv3.ph4.bg-near-black.ba.br3.bw1.b--white-10.overflow-x-hidden",
{},
// message body is a collection of hiccup elements (parsed & cleaned from HTML)
// see: transformContent() & transformMessage() functions
...content,
// media previews
mediaAttachments(media, mediaSel),
// message metadata
div(
".f7.gray",
{},
anchor(".link.gray", {}, `↺ ${unitless(replies)}`),
anchor(".link.gray.ml3", {}, `❤️ ${unitless(likes)}`),
anchor(".link.gray.ml3", {}, `🚀 ${unitless(boosts)}`),
anchor(".link.gray.ml3", { href: url }, fmt(date))
)
);
Loading

0 comments on commit f6caccd

Please sign in to comment.