import { blueskyService } from "./services";
import type {
SessionInfo,
GeneratorDefinition,
FeedGeneratorDescription,
FeedItemViewModel,
FeedSignalRecord,
FeedAuthor,
FeedItem,
TasteProfileResult,
SelectableOption,
} from "./types";
// Simple reactive store that works with Lit
class Store {
// Session state
session: SessionInfo = {
isAuthenticated: false,
did: null,
handle: null,
pdsUrl: null,
};
// UI state
busy = false;
error: string | null = null;
status: string | null = null;
loginIdentifier = "";
// Feed state
generators: GeneratorDefinition[] = [];
selectedGeneratorId = "balanced";
feedGeneratorInfo: FeedGeneratorDescription | null = null;
// Feed items and signals
items: FeedItemViewModel[] = [];
signals: FeedSignalRecord[] = [];
// Like feed state
likeFeed: {
importedPostCount: number;
importedAuthorCount: number;
items: FeedItem[];
authors: FeedAuthor[];
} = {
importedPostCount: 0,
importedAuthorCount: 0,
items: [],
authors: [],
};
// Taste profile
tasteProfile: TasteProfileResult | null = null;
// Options for tuning
includeOptions: SelectableOption[] = [
{ key: "dev", title: "Developer posts", description: "Build logs and engineering notes.", isSelected: true },
{ key: "science", title: "Science", description: "Research threads and explainers.", isSelected: true },
{ key: "design", title: "Design", description: "UI, product, and systems thinking.", isSelected: true },
{ key: "local", title: "Posts like my likes", description: "Items similar to positive signals.", isSelected: false },
];
suppressOptions: SelectableOption[] = [
{ key: "ragebait", title: "Ragebait", description: "Repeated outrage loops.", isSelected: true },
{ key: "promo", title: "Heavy self-promo", description: "Thin promotional posting.", isSelected: true },
{ key: "spoilers", title: "Spoilers", description: "Entertainment spoilers.", isSelected: false },
{ key: "low-context", title: "Low-context reposts", description: "Posts with weak standalone value.", isSelected: false },
];
// Callbacks for reactivity
private listeners = new Set<() => void>();
subscribe(callback: () => void) {
this.listeners.add(callback);
return () => this.listeners.delete(callback);
}
private notify() {
this.listeners.forEach(cb => cb());
}
// Computed getters
get isAuthenticated(): boolean {
return this.session.isAuthenticated;
}
get sessionStatusText(): string {
return this.session.isAuthenticated
? `Signed in as ${this.session.handle}`
: "Signed out";
}
get selectedGenerator(): GeneratorDefinition | undefined {
return this.generators.find((g) => g.id === this.selectedGeneratorId);
}
// Actions
setSession(session: SessionInfo) {
this.session = session;
this.notify();
}
setError(err: string | null) {
this.error = err;
if (err) {
this.status = null;
}
this.notify();
}
setStatus(msg: string | null) {
this.status = msg;
if (msg) {
this.error = null;
}
this.notify();
}
setBusy(value: boolean) {
this.busy = value;
this.notify();
}
setLoginIdentifier(value: string) {
this.loginIdentifier = value;
this.notify();
}
setGenerators(generators: GeneratorDefinition[]) {
this.generators = generators;
this.notify();
}
setSelectedGeneratorId(id: string) {
this.selectedGeneratorId = id;
this.notify();
}
setFeedGeneratorInfo(info: FeedGeneratorDescription | null) {
this.feedGeneratorInfo = info;
this.notify();
}
setLikeFeed(likeFeed: {
importedPostCount: number;
importedAuthorCount: number;
items: FeedItem[];
authors: FeedAuthor[];
}) {
this.likeFeed = likeFeed;
this.notify();
}
setItems(items: FeedItemViewModel[]) {
this.items = items;
this.notify();
}
setSignals(signals: FeedSignalRecord[]) {
this.signals = signals;
this.notify();
}
setTasteProfile(profile: TasteProfileResult | null) {
this.tasteProfile = profile;
this.notify();
}
toggleIncludeOption(key: string) {
const opts = this.includeOptions;
const idx = opts.findIndex((o) => o.key === key);
if (idx >= 0) {
const opt = opts[idx];
const newOpts = [...opts];
newOpts[idx] = { ...opt, isSelected: !opt.isSelected };
this.includeOptions = newOpts;
this.notify();
}
}
toggleSuppressOption(key: string) {
const opts = this.suppressOptions;
const idx = opts.findIndex((o) => o.key === key);
if (idx >= 0) {
const opt = opts[idx];
const newOpts = [...opts];
newOpts[idx] = { ...opt, isSelected: !opt.isSelected };
this.suppressOptions = newOpts;
this.notify();
}
}
buildSummaryBadges(): string[] {
const output: string[] = [`Mode: ${this.selectedGeneratorId}`];
for (const opt of this.includeOptions) {
if (opt.isSelected) output.push(`+ ${opt.title}`);
}
for (const opt of this.suppressOptions) {
if (opt.isSelected) output.push(`- ${opt.title}`);
}
return output;
}
}
export const store = new Store();
// Auto-restore session on load
(async () => {
try {
const sess = await blueskyService.getSession();
if (sess.isAuthenticated) {
console.log("[store] Session auto-restored:", sess);
store.setSession(sess);
}
} catch (err) {
console.log("[store] No session to restore");
}
})();
// Helper functions for URLs
export function toBlueskyPostUrl(item: { subjectUri: string; authorHandle?: string; authorDid: string }): string {
const segments = item.subjectUri.split("/").filter(Boolean);
const postKey = segments[segments.length - 1] ?? item.subjectUri;
const profile = item.authorHandle || item.authorDid;
return `https://bsky.app/profile/${encodeURIComponent(profile)}/post/${encodeURIComponent(postKey)}`;
}
export function toBlueskyProfileUrl(did: string, handle?: string): string {
const profile = handle || did;
return `https://bsky.app/profile/${encodeURIComponent(profile)}`;
}
export function shortPostId(subjectUri: string): string {
const segments = subjectUri.split("/").filter(Boolean);
return segments[segments.length - 1] ?? subjectUri;
}
export function formatTimestamp(createdAt: string | null): string {
if (!createdAt) return "time unknown";
const date = new Date(createdAt);
return date.toLocaleString();
}