~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

Bots wear a robot after their name, and on their avatar in a stack

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:web/bot-mark
crates/knotless-serve/src/web/actor.rs+6−1
@@ -11,15 +11,20 @@ pub struct ActorView {
pub handle: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar: Option<String>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub bot: bool,
}
/// `did`, with its handle and its picture's address in full.
/// `did`, with its handle, its picture's address in full, and whether the
/// index has it saying it is a bot.
pub async fn actor(app: &App, did: &str) -> ActorView {
let avatar = avatar(app, did).await;
let bot = app.index.as_ref().and_then(|i| i.is_bot(did).ok());
ActorView {
did: did.to_string(),
handle: handle(app, did).await,
avatar: avatar.map(absolute),
bot: bot.unwrap_or(false),
}
}
crates/knotless-serve/src/index.rs+9−0
@@ -1954,6 +1954,13 @@ impl Index {
rows?.collect()
}
/// Whether `did`'s knotless profile says it is a bot.
pub fn is_bot(&self, did: &str) -> rusqlite::Result<bool> {
let db = self.readers.take(&self.db);
let mut stmt = db.prepare_cached("SELECT 1 FROM bot WHERE did = ?1")?;
stmt.exists([did])
}
/// `viewer`'s follow naming `did`, by its URI, if they follow it.
pub fn follow_of(&self, viewer: &str, did: &str) -> rusqlite::Result<Option<String>> {
let db = self.readers.take(&self.db);
@@ -6319,7 +6326,9 @@ mod tests {
.map(|p| p.by)
.collect();
assert_eq!(by, ["did:a", "did:a"]);
assert!(index.is_bot("did:b").unwrap() && !index.is_bot("did:a").unwrap());
index.delete("did:b", nsid::PROFILE, "self").unwrap();
assert!(!index.is_bot("did:b").unwrap());
let of = |did| {
index
.pushes(10, None, Some(did), None, false)
docs/reference/api.md+7−2
@@ -135,6 +135,11 @@ with `-`, or one holding a NUL, newline or space is 400 `bad revision
XRPC queries under `/xrpc/`. All but `getProfile`, `describeServer` and `getStats` need `--index`, and
without one answer 404 `this server keeps no index`.
An account shown beside what it did or holds (`dev.knotless.defs#actorView`:
an owner, an author, a collaborator, a reviewer) is `{did, handle, avatar?,
bot?}`, where `bot` is true when its `dev.knotless.profile` says it is a bot,
as the index keeps of every account.
### `dev.knotless.listRepos`
| Parameter | Default | |
@@ -186,7 +191,7 @@ Zoekt cannot read answers 400, and a server without `--code-index` 501.
| `q` | | required; matched against the handle, ignoring case |
| `limit` | 10 | clamped to 1 to 50 |
Answers `owners`, each an `owner` (`did`, `handle`, `avatar?`) and its
Answers `owners`, each an `owner` (`did`, `handle`, `avatar?`, `bot?`) and its
`repoCount`: the accounts owning repositories `listRepos` lists, those whose
handle starts with `q` first, then the one with the newest repository. Owners
are matched by the handles the index keeps; an owner whose handle was never
@@ -241,7 +246,7 @@ names now count.
| `limit` | 40 | clamped to 1 to 100 |
| `cursor` | | from the page before |
Answers `events` (`createdAt`, `actor` `{did, handle, avatar?}`, `repo`
Answers `events` (`createdAt`, `actor` `{did, handle, avatar?, bot?}`, `repo`
`{did, handle, name}`, `action`) and `cursor`. `action.$type` is one of:
| `$type` | Fields |
lexicons/dev/knotless/defs.json+2−1
@@ -64,7 +64,8 @@
"properties": {
"did": { "type": "string", "format": "did" },
"handle": { "type": "string", "description": "The handle the account claims, when it resolves back to it; else the DID." },
"avatar": { "type": "string", "format": "uri", "description": "The account's picture, as in dev.knotless.getProfile#profileView." }
"avatar": { "type": "string", "format": "uri", "description": "The account's picture, as in dev.knotless.getProfile#profileView." },
"bot": { "type": "boolean", "description": "Whether its knotless profile says it is a bot, as the appview has indexed. Absent when it does not, and from an appview that keeps no index." }
}
},
"themeView": {
lexicons/dev/knotless/getFeed.json+2−1
@@ -48,7 +48,8 @@
"properties": {
"did": { "type": "string", "format": "did" },
"handle": { "type": "string", "description": "The handle the account claims, when it resolves back to it; else the DID." },
"avatar": { "type": "string", "format": "uri", "description": "The account's picture, as in dev.knotless.getProfile#profileView." }
"avatar": { "type": "string", "format": "uri", "description": "The account's picture, as in dev.knotless.getProfile#profileView." },
"bot": { "type": "boolean", "description": "Whether its knotless profile says it is a bot, as in dev.knotless.defs#actorView." }
}
},
"repoView": {
web/src/lib/atoms/Avatar.svelte+30−11
@@ -7,33 +7,52 @@
<script lang="ts">
import { hue, identicon } from '#lib/format.ts';
import Icon from '#lib/atoms/Icon.svelte';
/**
* A round mark: the picture at `src`, or else a pattern in a color that
* `did` picks, the account's own wherever it shows, or `name` for one
* with no account.
* with no account. An account that says it is a bot (`bot`) wears a
* robot on its corner, the one `.av` holding the picture and the robot.
*/
let {
name,
did,
size,
src
}: { name: string; did?: string; size?: 'lg' | 'xl'; src?: string | null } = $props();
src,
bot
}: {
name: string;
did?: string;
size?: 'lg' | 'xl';
src?: string | null;
bot?: boolean;
} = $props();
let cls = $derived(size ? `av ${size}` : 'av');
const seed = $derived(did ?? name);
/** The thumbnail wherever it is drawn small: a twentieth of the bytes. */
const shown = $derived(size === 'xl' ? src : src?.replace(BLUESKY_AVATAR, THUMBNAIL));
</script>
{#if shown}<img class={cls} src={shown} alt="" />{:else}<svg
{#snippet pattern()}{#each identicon(seed) as [x, y] (`${x},${y}`)}<rect
{x}
{y}
width="1"
height="1"
/>{/each}{/snippet}
{#if bot}<span
class="{cls} bot{shown ? '' : ' id'}"
style={shown ? undefined : `--h:${hue(seed)}`}
title="Bot"
>{#if shown}<img src={shown} alt="" />{:else}<svg
viewBox="-1 -1 7 7"
shape-rendering="crispEdges"
aria-hidden="true">{@render pattern()}</svg
>{/if}<span class="av-badge"><Icon name="bot" /></span></span
>{:else if shown}<img class={cls} src={shown} alt="" />{:else}<svg
class="{cls} id"
style="--h:{hue(seed)}"
viewBox="-1 -1 7 7"
shape-rendering="crispEdges"
aria-hidden="true"
>{#each identicon(seed) as [x, y] (`${x},${y}`)}<rect
{x}
{y}
width="1"
height="1"
/>{/each}</svg
aria-hidden="true">{@render pattern()}</svg
>{/if}
web/src/lib/atoms/BotMark.sveltenew+6−0
@@ -0,0 +1,6 @@
<script lang="ts">
import Icon from '#lib/atoms/Icon.svelte';
/** The robot after the name of an account that says it is a bot. */
</script>
<span class="bot-mark" role="img" aria-label="Bot" title="Bot"><Icon name="bot" /></span>
web/src/lib/molecules/AssigneesFact.svelte+5−2
@@ -1,6 +1,7 @@
<script lang="ts">
import type { ActorView } from '#lib/lexicons/dev/knotless/defs.ts';
import Avatar from '#lib/atoms/Avatar.svelte';
import BotMark from '#lib/atoms/BotMark.svelte';
import Icon from '#lib/atoms/Icon.svelte';
/**
* Whom a pull or an issue is assigned to, as a fact of it: their faces
@@ -39,7 +40,7 @@
<summary aria-label="Assigned to {assignees.map((a) => a.handle).join(', ')}">
<span class="avs">
{#each assignees.slice(0, SHOWN).reverse() as a (a.did)}<span class="face"
><Avatar name={a.handle} did={a.did} src={a.avatar} /></span
><Avatar name={a.handle} did={a.did} src={a.avatar} bot={a.bot} /></span
>{/each}
</span>
<span class="said">Assigned to <b>{who}</b></span>
@@ -49,7 +50,9 @@
{#each assignees as a (a.did)}
<div class="menu-item reviewer">
<Avatar name={a.handle} did={a.did} src={a.avatar} />
<span class="who">{a.handle}</span>
<span class="who"
>{a.handle}{#if a.bot}<BotMark />{/if}</span
>
{#if assign}<button
class="stop"
type="button"
web/src/lib/molecules/Crowd.svelte+5−2
@@ -2,9 +2,11 @@
import type * as getFeed from '#lib/lexicons/dev/knotless/getFeed.ts';
import { urlPath } from '#lib/format.ts';
import Avatar from '#lib/atoms/Avatar.svelte';
import BotMark from '#lib/atoms/BotMark.svelte';
import Icon from '#lib/atoms/Icon.svelte';
/** Several accounts who did one thing: a row of their pictures, the first few, opening to all of them by name. */
let { actors }: { actors: Pick<getFeed.ActorView, 'did' | 'handle' | 'avatar'>[] } = $props();
let { actors }: { actors: Pick<getFeed.ActorView, 'did' | 'handle' | 'avatar' | 'bot'>[] } =
$props();
const SHOWN = 8;
const more = $derived(actors.length - SHOWN);
</script>
@@ -16,6 +18,7 @@
name={a.handle}
did={a.did}
src={a.avatar}
bot={a.bot}
/>{/each}</span
>{#if more > 0}<span class="more">+{more}</span>{/if}<Icon name="caret" /></summary
>
@@ -23,7 +26,7 @@
{#each actors as a (a.did)}<li>
<Avatar name={a.handle} did={a.did} src={a.avatar} /><a href="/{urlPath(a.handle)}"
>{a.handle}</a
>
>{#if a.bot}<BotMark />{/if}
</li>{/each}
</ul>
</details>
web/src/lib/molecules/FeedItem.svelte+2−1
@@ -17,6 +17,7 @@
urlPath
} from '#lib/format.ts';
import Avatar from '#lib/atoms/Avatar.svelte';
import BotMark from '#lib/atoms/BotMark.svelte';
import Icon from '#lib/atoms/Icon.svelte';
import Ref from '#lib/atoms/Ref.svelte';
import Time from '#lib/atoms/Time.svelte';
@@ -109,7 +110,7 @@
{#snippet where()}{' on '}<RepoLink owner={x.repo.handle} name={x.repo.name} />{/snippet}
{#snippet who()}{#if !own}<a class="who" href="/{urlPath(x.actor.handle)}">{x.actor.handle}</a
>{/if}{own ? '' : ' '}{/snippet}
>{#if x.actor.bot}<BotMark />{/if}{/if}{own ? '' : ' '}{/snippet}
{#snippet rest(
b: { count: number; merges: number; elsewhere: number; ref: string; to?: string },
listed: number
web/src/lib/molecules/ItemRow.svelte+1−0
web/src/lib/molecules/Person.svelte+4−3
web/src/lib/molecules/RepoPeople.svelte+5−1
web/src/lib/molecules/ReviewersFact.svelte+4−3
web/src/lib/organisms/ActorList.svelte+2−1
web/src/lib/organisms/Comment.svelte+4−1
web/src/lib/organisms/LineThread.svelte+1−1
web/src/lib/organisms/MemberList.svelte+1−1
web/src/lib/organisms/ProfileCard.svelte+4−4
web/src/lib/organisms/QueueLine.svelte+1−1
web/src/lib/organisms/Reviewers.svelte+6−2
web/src/lib/icons.ts+4−1
web/src/routes/[authority]/[repo]/[kind=item]/[did]/[rkey]/files/+page.svelte+3−3
web/src/routes/[authority]/[repo]/[kind=item]/[did]/[rkey]/+page.svelte+7−5
web/src/app.css+49−2