-
-
Notifications
You must be signed in to change notification settings - Fork 153
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(examples): add mastodon-feed example
- Loading branch information
1 parent
90a7f0b
commit f6caccd
Showing
14 changed files
with
782 additions
and
0 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", {}) | ||
) | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
) | ||
); |
Oops, something went wrong.