view who was fronting when a record was made

feat: actually work properly on forks, refactor ui

ptr.pet 739597f6 823fd030

verified
Changed files
+271 -121
src
+1
.gitignore
···
.wxt
.wxt-runner
web-ext.config.ts
+
web-ext-artifacts
# Editor directories and files
.vscode/*
+1 -1
package.json
···
"name": "at-fronter",
"description": "view who was fronting when a record was made",
"private": true,
-
"version": "0.0.0",
+
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "wxt",
+18 -12
src/entrypoints/background.ts
···
import { expect } from "@/lib/result";
import {
type Fronter,
+
fronterGetSocialAppHrefs,
fronterGetSocialAppHref,
getFronter,
getSpFronters,
···
browser.tabs.sendMessage(sender.tab?.id!, {
type: "TIMELINE_FRONTER",
results: new Map(
-
results.map((fronter) => [
-
fronterGetSocialAppHref(fronter, fronter.rkey),
-
fronter,
-
]),
+
results.flatMap((fronter) =>
+
fronterGetSocialAppHrefs(fronter, fronter.rkey).map((href) => [
+
href,
+
fronter,
+
]),
+
),
),
});
};
···
(await Promise.allSettled(allPromises))
.filter((result) => result.status === "fulfilled")
.flatMap((result) => result.value ?? [])
-
.map((fronter) => [
-
fronterGetSocialAppHref(fronter, fronter.rkey),
-
fronter,
-
]),
+
.flatMap((fronter) =>
+
fronterGetSocialAppHrefs(fronter, fronter.rkey).map((href) => [
+
href,
+
fronter,
+
]),
+
),
);
browser.tabs.sendMessage(sender.tab?.id!, {
type: "TIMELINE_FRONTER",
···
(await Promise.allSettled(promises))
.filter((result) => result.status === "fulfilled")
.flatMap((result) => result.value ?? [])
-
.map((fronter) => [
-
fronterGetSocialAppHref(fronter, fronter.rkey, fronter.depth),
-
fronter,
-
]),
+
.flatMap((fronter) =>
+
fronterGetSocialAppHrefs(fronter, fronter.rkey, fronter.depth).map(
+
(href) => [href, fronter],
+
),
+
),
);
browser.tabs.sendMessage(sender.tab?.id!, {
type: "THREAD_FRONTER",
+5 -1
src/entrypoints/content.ts
···
import {
Fronter,
fronterGetSocialAppHref,
+
fronterGetSocialAppHrefs,
parseSocialAppPostUrl,
} from "@/lib/utils";
import { parseResourceUri, ResourceUri } from "@atcute/lexicons";
···
fronters.entries().flatMap(([uri, fronter]) => {
if (!fronter) return [];
const rkey = expect(parseResourceUri(uri)).rkey!;
-
return [[fronterGetSocialAppHref(fronter, rkey), fronter]];
+
return fronterGetSocialAppHrefs(fronter, rkey).map((href) => [
+
href,
+
fronter,
+
]);
}),
);
applyFrontersToPage(updated);
+163 -99
src/entrypoints/popup/App.svelte
···
let isQuerying = $state(false);
let fronterName = $state("");
let spToken = $state("");
+
let isFromCurrentTab = $state(false);
const makeOutput = (fronter: any) => {
-
return `HANDLE: ${fronter.handle ?? "handle.invalid"}<br>FRONTER: ${fronter.fronterName}`;
+
return `HANDLE: ${fronter.handle ?? "handle.invalid"}<br>FRONTER(S): ${fronter.names.join(", ")}`;
};
const queryRecord = async (recordUri: ResourceUri) => {
···
const clearResult = () => {
queryResult = "";
recordAtUri = "";
+
isFromCurrentTab = false;
};
onMount(async () => {
···
if (tabFronter) {
queryResult = makeOutput(tabFronter);
recordAtUri = tabFronter.recordUri;
+
isFromCurrentTab = true;
}
});
</script>
<main>
<div class="container">
-
<header class="header">
-
<div class="title">AT_FRONTER</div>
-
</header>
-
<div class="content">
<section class="query-panel">
<div class="panel-header">
···
<div class="output-container">
<div class="output-header">
-
<span>OUTPUT</span>
+
<div class="output-header-left">
+
<span>OUTPUT</span>
+
{#if isFromCurrentTab}
+
<div class="tab-indicator">
+
<span class="tab-indicator-text"
+
>FROM_CURRENT_TAB</span
+
>
+
<div class="tab-indicator-accent"></div>
+
</div>
+
{/if}
+
</div>
<div class="clear-button-container">
{#if queryResult && !isQuerying}
<button
···
<span class="panel-title">CONFIGURATION</span>
<div class="panel-accent"></div>
</div>
-
-
<div class="config-row">
-
<span class="config-label">FRONTER_NAME</span>
-
<div class="config-input-wrapper">
+
<div class="config-card">
+
<div class="config-row">
+
<span class="config-label">SP_TOKEN</span>
+
<input
+
type="password"
+
placeholder="enter_simply_plural_token"
+
oninput={updateSpToken}
+
bind:value={spToken}
+
class="config-input"
+
class:has-value={spToken}
+
/>
+
</div>
+
<div class="config-note">
+
<span class="note-text">
+
token requires only read permissions
+
</span>
+
</div>
+
</div>
+
<div class="config-card">
+
<div class="config-row">
+
<span class="config-label">FRONTER_NAME</span>
<input
type="text"
placeholder="enter_identifier"
···
class:has-value={fronterName}
/>
</div>
-
</div>
-
-
<div class="config-row">
-
<span class="config-label">SP_TOKEN</span>
-
<div class="config-input-wrapper">
-
<input
-
type="password"
-
placeholder="enter_simply_plural_token"
-
oninput={updateSpToken}
-
bind:value={spToken}
-
class="config-input"
-
class:has-value={spToken}
-
/>
+
<div class="config-note">
+
<span class="note-text">
+
overrides Simply Plural fronters when set
+
</span>
</div>
</div>
</section>
</div>
<footer class="footer">
-
<span
-
>SOURCE ON <a
-
href="https://tangled.sh/did:plc:dfl62fgb7wtjj3fcbb72naae/at-fronter"
-
>TANGLED</a
-
></span
+
<span class="title">AT_FRONTER</span>
+
<span class="footer-separator">•</span>
+
<span class="footer-source">SOURCE ON </span>
+
<a
+
href="https://tangled.sh/did:plc:dfl62fgb7wtjj3fcbb72naae/at-fronter"
+
class="footer-link">TANGLED</a
>
</footer>
</div>
···
background: linear-gradient(180deg, #000000 0%, #0a0a0a 100%);
}
-
.header {
-
display: flex;
-
align-items: center;
-
justify-content: center;
-
padding: 20px 20px;
-
background: #000000;
-
border-bottom: 1px solid #333333;
-
position: relative;
-
}
-
-
.header::after {
-
content: "";
-
position: absolute;
-
bottom: 0;
-
left: 0;
-
width: 100%;
-
height: 1px;
-
background: linear-gradient(90deg, transparent, #555555, transparent);
-
}
-
.title {
-
font-size: 18px;
-
font-weight: 800;
-
letter-spacing: 3px;
-
color: #ffffff;
+
font-size: 10px;
+
font-weight: 700;
+
letter-spacing: 2px;
+
color: #999999;
+
line-height: 1;
+
vertical-align: baseline;
}
.content {
flex: 1;
display: flex;
flex-direction: column;
-
gap: 24px;
-
padding: 24px 20px;
+
gap: 20px;
+
padding: 18px 16px;
overflow-y: auto;
}
···
.config-panel {
display: flex;
flex-direction: column;
-
gap: 16px;
+
gap: 12px;
+
}
+
+
.config-card {
+
background: #0d0d0d;
+
border: 1px solid #2a2a2a;
+
border-left: 3px solid #444444;
+
padding: 10px;
+
display: flex;
+
flex-direction: column;
+
gap: 6px;
+
transition: border-left-color 0.2s ease;
+
}
+
+
.config-card:hover {
+
border-left-color: #555555;
+
}
+
+
.config-note {
+
padding: 0;
+
background: transparent;
+
border: none;
+
margin: 0;
+
}
+
+
.note-text {
+
font-size: 11px;
+
color: #bbbbbb;
+
line-height: 1.3;
+
font-weight: 500;
+
letter-spacing: 0.5px;
}
.panel-header {
···
.record-input {
flex: 1;
-
padding: 16px 18px;
+
padding: 12px 14px;
background: transparent;
border: none;
outline: none;
···
.exec-button {
position: relative;
-
padding: 16px 28px;
+
padding: 8px 10px;
background: #2a2a2a;
border: none;
border-left: 1px solid #444444;
···
min-height: 32px;
}
+
.output-header-left {
+
display: flex;
+
align-items: center;
+
gap: 12px;
+
}
+
+
.tab-indicator {
+
display: flex;
+
align-items: center;
+
gap: 6px;
+
padding: 4px 8px;
+
background: #1a1a1a;
+
border: 1px solid #333333;
+
position: relative;
+
overflow: hidden;
+
}
+
+
.tab-indicator-text {
+
font-size: 9px;
+
color: #00ff41;
+
font-weight: 700;
+
letter-spacing: 1px;
+
position: relative;
+
z-index: 1;
+
}
+
+
.tab-indicator-accent {
+
position: absolute;
+
left: 0;
+
bottom: 0;
+
width: 100%;
+
height: 1px;
+
background: #00ff41;
+
animation: pulse 2s ease-in-out infinite;
+
}
+
.clear-button-container {
width: 60px;
display: flex;
···
}
.output-content {
-
padding: 18px;
+
padding: 14px;
height: 100%;
display: flex;
align-items: center;
···
.config-row {
display: flex;
-
flex-direction: column;
-
gap: 8px;
+
align-items: center;
+
gap: 12px;
+
margin-bottom: 0;
}
.config-label {
-
font-size: 11px;
-
color: #aaaaaa;
-
letter-spacing: 1.5px;
+
font-size: 12px;
+
color: #cccccc;
+
letter-spacing: 1px;
font-weight: 700;
-
}
-
-
.config-input-wrapper {
-
display: flex;
-
align-items: center;
+
white-space: nowrap;
+
min-width: 90px;
}
.config-input {
flex: 1;
-
padding: 14px 18px;
+
padding: 10px 12px;
background: #181818;
border: 1px solid #333333;
color: #ffffff;
font-family: inherit;
-
font-size: 13px;
+
font-size: 12px;
font-weight: 500;
transition: all 0.2s ease;
position: relative;
···
.footer {
display: flex;
-
align-items: center;
+
align-items: baseline;
justify-content: center;
-
padding: 16px 20px;
+
gap: 8px;
+
padding: 12px 16px;
background: #000000;
-
border-top: 1px solid #333333;
-
font-size: 10px;
-
color: #888888;
-
font-weight: 600;
-
letter-spacing: 1px;
+
border-top: 1px solid #222222;
+
font-size: 9px;
+
color: #666666;
+
font-weight: 500;
+
letter-spacing: 0.5px;
+
line-height: 1;
position: relative;
}
···
left: 0;
width: 100%;
height: 1px;
-
background: linear-gradient(90deg, transparent, #555555, transparent);
+
background: linear-gradient(90deg, transparent, #333333, transparent);
+
}
+
+
.footer-separator {
+
color: #444444;
+
font-weight: 400;
+
line-height: 1;
+
vertical-align: baseline;
+
}
+
+
.footer-source {
+
color: #777777;
+
line-height: 1;
+
vertical-align: baseline;
}
-
.footer a {
-
color: #aaaaaa;
+
.footer-link {
+
color: #999999;
text-decoration: none;
font-weight: 700;
transition: color 0.2s ease;
+
line-height: 1;
+
vertical-align: baseline;
}
-
.footer a:hover {
-
color: #ffffff;
+
.footer-link:hover {
+
color: #cccccc;
}
/* Animations */
···
100% {
left: 100%;
}
-
}
-
-
/* Scrollbar */
-
.content::-webkit-scrollbar {
-
width: 2px;
-
}
-
-
.content::-webkit-scrollbar-track {
-
background: #000000;
-
}
-
-
.content::-webkit-scrollbar-thumb {
-
background: #333333;
-
}
-
-
.content::-webkit-scrollbar-thumb:hover {
-
background: #555555;
}
</style>
+68 -3
src/entrypoints/popup/app.css
···
color: #ffffff;
}
+
/* Cross-browser scrollbar styling */
+
+
/* Standard scrollbar properties (Firefox, Chrome 121+, Edge 121+) */
+
* {
+
scrollbar-width: thin;
+
scrollbar-color: #333333 #0a0a0a;
+
}
+
+
/* Content areas get even thinner scrollbars */
+
.content,
+
.output-content,
+
textarea,
+
input {
+
scrollbar-width: thin;
+
scrollbar-color: #2a2a2a #000000;
+
}
+
+
/* Webkit scrollbar styling for older browsers and better customization */
/* Global scrollbar styling */
::-webkit-scrollbar {
-
width: 2px;
-
height: 2px;
+
width: 8px;
+
height: 8px;
}
::-webkit-scrollbar-track {
-
background: #000000;
+
background: #0a0a0a;
+
border-radius: 0;
}
::-webkit-scrollbar-thumb {
background: #333333;
+
border-radius: 0;
border: none;
+
transition: background 0.2s ease;
}
::-webkit-scrollbar-thumb:hover {
background: #555555;
}
+
::-webkit-scrollbar-thumb:active {
+
background: #666666;
+
}
+
::-webkit-scrollbar-corner {
+
background: #0a0a0a;
+
}
+
+
/* Scrollbar for specific containers */
+
.content::-webkit-scrollbar,
+
.output-content::-webkit-scrollbar,
+
textarea::-webkit-scrollbar,
+
input::-webkit-scrollbar {
+
width: 6px;
+
height: 6px;
+
}
+
+
.content::-webkit-scrollbar-track,
+
.output-content::-webkit-scrollbar-track,
+
textarea::-webkit-scrollbar-track,
+
input::-webkit-scrollbar-track {
background: #000000;
+
}
+
+
.content::-webkit-scrollbar-thumb,
+
.output-content::-webkit-scrollbar-thumb,
+
textarea::-webkit-scrollbar-thumb,
+
input::-webkit-scrollbar-thumb {
+
background: #2a2a2a;
+
border-radius: 0;
+
border: none;
+
transition: background 0.15s ease;
+
}
+
+
.content::-webkit-scrollbar-thumb:hover,
+
.output-content::-webkit-scrollbar-thumb:hover,
+
textarea::-webkit-scrollbar-thumb:hover,
+
input::-webkit-scrollbar-thumb:hover {
+
background: #444444;
+
}
+
+
.content::-webkit-scrollbar-thumb:active,
+
.output-content::-webkit-scrollbar-thumb:active,
+
textarea::-webkit-scrollbar-thumb:active,
+
input::-webkit-scrollbar-thumb:active {
+
background: #555555;
}
/* Animations */
+15 -5
src/lib/utils.ts
···
}));
};
-
export const fronterGetSocialAppHref = (
+
export const fronterGetSocialAppHrefs = (
fronter: Fronter,
rkey: RecordKey,
depth?: number,
) => {
-
// console.log("fronterGetSocialAppHref", fronter, rkey, depth);
-
return depth === 0
-
? `/profile/${fronter.handle ?? fronter.did}`
-
: `/profile/${fronter.handle ?? fronter.did}/post/${rkey}`;
+
return [
+
fronter.handle
+
? [fronterGetSocialAppHref(fronter.handle, rkey, depth)]
+
: [],
+
fronterGetSocialAppHref(fronter.did, rkey, depth),
+
].flat();
+
};
+
+
export const fronterGetSocialAppHref = (
+
repo: string,
+
rkey: RecordKey,
+
depth?: number,
+
) => {
+
return depth === 0 ? `/profile/${repo}` : `/profile/${repo}/post/${rkey}`;
};
export const parseSocialAppPostUrl = (url: string) => {