Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds curator descriptions and how it works modal #17

Merged
merged 6 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ export async function main() {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
connectSrc: ["'self'", "ws:", "wss:"], // Allow WebSocket connections
scriptSrc: ["'self'", "'unsafe-inline'"], // Required for some frontend frameworks
styleSrc: ["'self'", "'unsafe-inline'"], // Required for styled-components
imgSrc: ["'self'", "data:", "https:"], // Allow images from HTTPS sources
Expand Down Expand Up @@ -125,26 +124,26 @@ export async function main() {
: db.getAllSubmissions();
})
.get(
"/api/feed/:hashtagId",
"/api/submissions/:hashtagId",
({ params: { hashtagId } }: { params: { hashtagId: string } }) => {
const config = configService.getConfig();
const feed = config.feeds.find((f) => f.id === hashtagId);
if (!feed) {
throw new Error(`Feed not found: ${hashtagId}`);
}

// this should be pending submissions
return db.getSubmissionsByFeed(hashtagId);
},
)
.get(
"/api/submissions/:hashtagId",
"/api/feed/:hashtagId",
({ params: { hashtagId } }: { params: { hashtagId: string } }) => {
const config = configService.getConfig();
const feed = config.feeds.find((f) => f.id === hashtagId);
if (!feed) {
throw new Error(`Feed not found: ${hashtagId}`);
}
// this should be pending submissions

return db.getSubmissionsByFeed(hashtagId);
},
)
Expand All @@ -165,6 +164,10 @@ export async function main() {
const config = configService.getConfig();
return config.feeds;
})
.get("/api/config", () => {
const config = configService.getConfig();
return config;
})
.get(
"/api/config/:feedId",
({ params: { feedId } }: { params: { feedId: string } }) => {
Expand Down
2 changes: 2 additions & 0 deletions backend/src/services/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export const submissions = table(
tweetId: text("tweet_id").primaryKey(),
userId: text("user_id").notNull(),
username: text("username").notNull(),
curatorId: text("curator_id").notNull(),
curatorUsername: text("curator_username").notNull(),
content: text("content").notNull(),
description: text("description"),
status: text("status")
Expand Down
4 changes: 3 additions & 1 deletion backend/src/services/submissions/submission.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class SubmissionService {
private readonly twitterService: TwitterService,
private readonly DistributionService: DistributionService,
private readonly config: AppConfig,
) { }
) {}

async initialize(): Promise<void> {
// Initialize feeds and admin cache from config
Expand Down Expand Up @@ -162,6 +162,8 @@ export class SubmissionService {
tweetId: originalTweet.id!,
userId: originalTweet.userId!,
username: originalTweet.username!,
curatorId: userId,
curatorUsername: tweet.username!,
content: originalTweet.text || "",
description: this.extractDescription(tweet),
status: this.config.global.defaultStatus as
Expand Down
20 changes: 13 additions & 7 deletions backend/src/services/twitter/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ export class TwitterService {
// Load last checked tweet ID from cache
this.lastCheckedTweetId = db.getTwitterCacheValue("last_tweet_id");

// Try to login with retries
// Try to login with max 2 retries
logger.info("Attempting Twitter login...");
while (true) {
for (let attempt = 0; attempt < 3; attempt++) {
try {
await this.client.login(
this.config.username,
Expand All @@ -71,18 +71,24 @@ export class TwitterService {
| undefined,
}));
db.setTwitterCookies(this.config.username, formattedCookies);
logger.info("Successfully logged in to Twitter");
break;
}
} catch (error) {
logger.error("Failed to login to Twitter, retrying...", error);
break;
logger.error(
`Failed to login to Twitter (attempt ${attempt + 1}/3)...`,
error,
);
}

// Wait before retrying
await new Promise((resolve) => setTimeout(resolve, 2000));
if (attempt < 2) {
// Wait before retrying
await new Promise((resolve) => setTimeout(resolve, 2000));
}
}

logger.info("Successfully logged in to Twitter");
// If we get here without breaking, all attempts failed
throw new Error("Failed to login to Twitter after 3 attempts");
} catch (error) {
logger.error("Failed to initialize Twitter client:", error);
throw error;
Expand Down
2 changes: 2 additions & 0 deletions backend/src/types/twitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ export interface TwitterSubmission {
tweetId: string;
userId: string;
username: string;
curatorId: string;
curatorUsername: string;
content: string;
description?: string;
status: "pending" | "approved" | "rejected";
Expand Down
1 change: 0 additions & 1 deletion frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ The frontend communicates with the [backend service](../backend/README.md) throu

- Submission handling via `/submit` endpoint
- Content retrieval through `/submissions`
- Real-time updates using polling (future: WebSocket support)

See the [Backend README](../backend/README.md) for detailed API documentation.

Expand Down
160 changes: 115 additions & 45 deletions frontend/src/components/FeedItem.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
import { HiExternalLink } from "react-icons/hi";
import { TwitterSubmission } from "../types/twitter";

const BOT_ID = "test_curation";
import { useBotId } from "../lib/config";

const getTweetUrl = (tweetId: string, username: string) => {
return `https://x.com/${username}/status/${tweetId}`;
};

const getTwitterIntentUrl = (
tweetId: string,
action: "approve" | "reject",
botId: string,
) => {
const baseUrl = "https://twitter.com/intent/tweet";
// Add in_reply_to_status_id parameter to make it a reply
const params = new URLSearchParams({
text: `@${botId} #${action}`,
in_reply_to: tweetId,
});
return `${baseUrl}?${params.toString()}`;
};

const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString();
};
Expand All @@ -32,9 +45,15 @@ interface FeedItemProps {
}

export const FeedItem = ({ submission }: FeedItemProps) => {
const botId = useBotId();
const tweetId =
submission.status === "pending"
? submission.acknowledgmentTweetId
: submission.moderationResponseTweetId;

return (
<div className="card">
<div className="flex justify-between items-start mb-4">
<div className="flex justify-between items-start">
<div className="flex-grow">
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
Expand All @@ -60,12 +79,35 @@ export const FeedItem = ({ submission }: FeedItemProps) => {
{formatDate(submission.createdAt)}
</span>
</div>
{(submission.status === "approved" ||
submission.status === "rejected") &&
submission.moderationHistory?.length > 0 && (
<div className="text-sm space-y-2">
</div>
<p className="text-lg mb-4 leading-relaxed body-text">
{submission.content}
</p>
</div>
<div>
{tweetId && (
<a
href={getTweetUrl(tweetId, botId)}
target="_blank"
rel="noopener noreferrer"
>
<StatusBadge status={submission.status} />
</a>
)}
</div>
</div>

<div className="mt-6 flex justify-between items-start gap-8">
<div className="flex-1">
{(submission.status === "approved" ||
submission.status === "rejected") &&
submission.moderationHistory?.length > 0 && (
<div className="p-4 border-2 border-gray-200 rounded-md bg-gray-50 mb-4">
<div className="flex items-center gap-2 mb-2">
<h4 className="heading-3">Moderation Notes</h4>
<span className="text-gray-400">·</span>
<div className="text-gray-600">
Moderated by{" "}
by{" "}
<a
href={`https://x.com/${submission.moderationHistory?.[submission.moderationHistory.length - 1]?.adminId}`}
target="_blank"
Expand All @@ -80,47 +122,75 @@ export const FeedItem = ({ submission }: FeedItemProps) => {
}
</a>
</div>
{submission.moderationHistory?.[
submission.moderationHistory.length - 1
]?.note && (
<div className="text-gray-700">
<span className="font-semibold">Moderation Note:</span>{" "}
{
submission.moderationHistory?.[
submission.moderationHistory.length - 1
]?.note
}
</div>
)}
</div>
)}
</div>
<p className="text-lg mb-4 leading-relaxed body-text">
{submission.content}
</p>
</div>
<div className="flex items-end gap-2 flex-col">
<a
href={getTweetUrl(
(submission.status === "pending"
? submission.acknowledgmentTweetId
: submission.moderationResponseTweetId) || "",
BOT_ID,
{submission.moderationHistory?.[
submission.moderationHistory.length - 1
]?.note && (
<p className="body-text text-gray-700">
{
submission.moderationHistory?.[
submission.moderationHistory.length - 1
]?.note
}
</p>
)}
</div>
)}
target="_blank"
rel="noopener noreferrer"
>
<StatusBadge status={submission.status} />
</a>
</div>
</div>

{submission.description && (
<div className="mb-4">
<h4 className="heading-3 mb-1">Curator's Notes:</h4>
<p className="body-text">{submission.description}</p>
{submission.status === "pending" && (
<div className="p-4 border-2 border-gray-200 rounded-md bg-gray-50">
<div className="flex items-center gap-2 mb-2">
<h4 className="heading-3">Curator's Notes</h4>
<span className="text-gray-400">·</span>
<div className="text-gray-600">
by{" "}
<a
href={`https://x.com/${submission.curatorUsername}`}
target="_blank"
rel="noopener noreferrer"
className="text-gray-800 hover:text-gray-600 transition-colors"
>
@{submission.curatorUsername}
</a>
</div>
</div>
<p className="body-text text-gray-700">
{submission.description}
</p>
</div>
)}
</div>
)}

{submission.status === "pending" &&
submission.acknowledgmentTweetId && (
<div className="flex flex-col gap-2">
<a
href={getTwitterIntentUrl(
submission.acknowledgmentTweetId,
"approve",
botId,
)}
target="_blank"
rel="noopener noreferrer"
className="px-3 py-1.5 bg-green-200 hover:bg-green-300 text-black rounded-md border-2 border-black shadow-sharp hover:shadow-sharp-hover transition-all duration-200 translate-x-0 translate-y-0 hover:-translate-x-0.5 hover:-translate-y-0.5 text-sm font-medium"
>
Approve
</a>
<a
href={getTwitterIntentUrl(
submission.acknowledgmentTweetId,
"reject",
botId,
)}
target="_blank"
rel="noopener noreferrer"
className="px-3 py-1.5 bg-red-200 hover:bg-red-300 text-black rounded-md border-2 border-black shadow-sharp hover:shadow-sharp-hover transition-all duration-200 translate-x-0 translate-y-0 hover:-translate-x-0.5 hover:-translate-y-0.5 text-sm font-medium"
>
Reject
</a>
</div>
)}
</div>
</div>
);
};
Expand Down
52 changes: 30 additions & 22 deletions frontend/src/components/FeedList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,31 +24,39 @@ const FeedList = () => {

return (
<div className="flex flex-col md:block">
<div className="flex justify-between items-center px-4 md:px-0 mb-4">
<div className="flex justify-between items-center my-4">
<h1 className="text-xl font-bold">Feeds</h1>
<span className="md:hidden text-gray-400 flex items-center">
<span className="mr-1">scroll</span>
<FaChevronRight className="h-3 w-3" />
</span>
{feeds.length > 0 && (
<span className="md:hidden text-gray-400 flex items-center">
<span className="mr-1">scroll</span>
<FaChevronRight className="h-3 w-3" />
</span>
)}
</div>
<nav className="flex md:block overflow-x-auto p-1">
{feeds?.map((feed) => (
<Link
key={feed.id}
to="/feed/$feedId"
params={{ feedId: feed.id }}
className={`flex-shrink-0 min-w-[200px] mx-2 md:mx-0 md:min-w-0 block px-4 py-2 text-sm border-2 border-black shadow-sharp transition-all duration-200 md:mb-2 ${
feedId === feed.id
? "bg-gray-100 text-black font-medium translate-x-0.5 translate-y-0.5 shadow-none"
: "text-gray-600 hover:shadow-sharp-hover hover:-translate-x-0.5 hover:-translate-y-0.5 hover:bg-gray-50"
}`}
>
<div className="flex items-center">
<span className="flex-1">{feed.name}</span>
<span className="text-xs text-gray-400">#{feed.hashtag}</span>
</div>
</Link>
))}
{feeds.length === 0 ? (
<div className="flex justify-center items-center md:p-8">
<p className="text-gray-500">No feeds found</p>
</div>
) : (
feeds.map((feed) => (
<Link
key={feed.id}
to="/feed/$feedId"
params={{ feedId: feed.id }}
className={`flex-shrink-0 min-w-[200px] mx-2 md:mx-0 md:min-w-0 block px-4 py-2 text-sm border-2 border-black shadow-sharp transition-all duration-200 md:mb-2 ${
feedId === feed.id
? "bg-gray-100 text-black font-medium translate-x-0.5 translate-y-0.5 shadow-none"
: "text-gray-600 hover:shadow-sharp-hover hover:-translate-x-0.5 hover:-translate-y-0.5 hover:bg-gray-50"
}`}
>
<div className="flex items-center justify-between">
<span className="flex-1">{feed.name}</span>
<span className="">#{feed.id}</span>
</div>
</Link>
))
)}
</nav>
</div>
);
Expand Down
Loading
Loading