Skip to content

Commit

Permalink
adds thread support
Browse files Browse the repository at this point in the history
  • Loading branch information
elliotBraem committed Jan 9, 2025
1 parent 578c2bc commit 0e0adc0
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 24 deletions.
120 changes: 104 additions & 16 deletions src/components/compose-post.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,62 @@
import { useState } from "react";

// This "widget" handles all of the editing for post content
// Calls "onSubmit" on "Post"
// Calls "onSubmit" with an array of post objects

export function ComposePost({ onSubmit }) {
const [text, setText] = useState("");
const [isThreadMode, setIsThreadMode] = useState(false);
const [posts, setPosts] = useState([{ text: "", image: null }]);
const [error, setError] = useState("");

const handleTextChange = (index, value) => {
const newPosts = [...posts];
newPosts[index] = { ...newPosts[index], text: value };
setPosts(newPosts);
};

const addThread = () => {
setPosts([...posts, { text: "", image: null }]);
};

const removeThread = (index) => {
if (posts.length > 1) {
const newPosts = posts.filter((_, i) => i !== index);
setPosts(newPosts);
}
};

const toggleMode = () => {
setIsThreadMode(!isThreadMode);
if (!isThreadMode) {
// Converting single post to multiple
const text = posts[0].text;
// Only split if there's actual content and it contains ---
if (text.trim() && text.includes('---')) {
const threads = text.split('---')
.map(t => t.trim())
.filter(t => t)
.map(text => ({ text, image: null }));
setPosts(threads.length > 0 ? threads : [{ text: "", image: null }]);
}
} else {
// Converting multiple posts to single
const combinedText = posts.map(p => p.text).filter(t => t.trim()).join('\n---\n');
setPosts([{ text: combinedText, image: null }]);
}
};

const handleSubmit = async () => {
if (!text.trim()) {
const nonEmptyPosts = posts.filter(p => p.text.trim());
if (nonEmptyPosts.length === 0) {
setError("Please enter your post text");
return;
}
try {
setError("");

await onSubmit(text);

setText("");
// If not in thread mode, just submit the first post's content
const finalPosts = isThreadMode ? nonEmptyPosts : [posts[0]];
await onSubmit(finalPosts);
setPosts([{ text: "", image: null }]);
} catch (err) {
setError("Failed to send post");
console.error("Post error:", err);
Expand All @@ -26,20 +65,69 @@ export function ComposePost({ onSubmit }) {

return (
<div className="space-y-3">
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="What's happening?"
maxLength={280}
className="w-full min-h-[150px] p-4 border-2 border-gray-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none shadow-[2px_2px_0_rgba(0,0,0,1)]"
/>
<div className="flex justify-end mb-2">
<button
onClick={toggleMode}
className="text-sm px-3 py-1 border-2 border-gray-800 hover:bg-gray-100 shadow-[2px_2px_0_rgba(0,0,0,1)]"
>
{isThreadMode ? "Single Post Mode" : "Thread Mode"}
</button>
</div>

{isThreadMode ? (
<div className="space-y-4">
{posts.map((post, index) => (
<div key={index} className="relative">
<textarea
value={post.text}
onChange={(e) => handleTextChange(index, e.target.value)}
placeholder={`Thread part ${index + 1}`}
maxLength={280}
className="w-full min-h-[150px] p-4 border-2 border-gray-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none shadow-[2px_2px_0_rgba(0,0,0,1)]"
/>
<div className="flex justify-between items-center mt-1">
<span className="text-sm text-gray-500">
{post.text.length}/280 characters
</span>
{posts.length > 1 && (
<button
onClick={() => removeThread(index)}
className="text-red-500 text-sm hover:text-red-700"
>
Remove
</button>
)}
</div>
{/* Future image upload UI would go here */}
</div>
))}
<button
onClick={addThread}
className="w-full py-2 border-2 border-gray-800 hover:bg-gray-100 shadow-[2px_2px_0_rgba(0,0,0,1)] text-sm"
>
+ Add Thread
</button>
</div>
) : (
<div>
<textarea
value={posts[0].text}
onChange={(e) => handleTextChange(0, e.target.value)}
placeholder="What's happening?"
maxLength={280 * 10} // Allow for multiple tweets worth in single mode
className="w-full min-h-[150px] p-4 border-2 border-gray-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none shadow-[2px_2px_0_rgba(0,0,0,1)]"
/>
{/* Future image upload UI would go here */}
</div>
)}

<div className="flex justify-between items-center">
<span className="text-sm text-gray-500">
{text.length}/280 characters
{isThreadMode ? `${posts.length} parts` : `${posts[0].text.length} characters`}
</span>
<button
onClick={handleSubmit}
disabled={!text.trim()}
disabled={posts.every(p => !p.text.trim())}
className="flex items-center gap-2 px-6 py-2 border-2 border-gray-800 hover:bg-gray-100 shadow-[2px_2px_0_rgba(0,0,0,1)] disabled:opacity-50 disabled:cursor-not-allowed"
>
Post
Expand Down
13 changes: 8 additions & 5 deletions src/pages/api/twitter/tweet.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ export default async function handler(req, res) {
return res.status(405).json({ error: "Method not allowed" });
}

const { text } = req.body;
if (!text?.trim()) {
return res.status(400).json({ error: "Tweet text is required" });
const { posts } = req.body;
if (!Array.isArray(posts) || posts.length === 0 || !posts.every(p => p.text?.trim())) {
return res.status(400).json({ error: "Valid posts array is required" });
}

const cookies = parse(req.headers.cookie || "");
Expand All @@ -20,8 +20,11 @@ export default async function handler(req, res) {

try {
const twitterService = await TwitterService.initialize();
await twitterService.tweet(accessToken, accessSecret, text);
res.status(200).json({ success: true });
const response = await twitterService.tweet(accessToken, accessSecret, posts);
res.status(200).json({
success: true,
data: Array.isArray(response) ? response : [response]
});
} catch (error) {
console.error("Tweet error:", error);
res.status(500).json({ error: "Failed to send tweet" });
Expand Down
10 changes: 9 additions & 1 deletion src/services/near-social.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,19 @@ export class NearSocialService {
this.wallet = wallet;
}

async createPost(content) {
async createPost(posts) {
const account = await this.wallet.getAccount();
const { publicKey, accountId } = account;

try {
// Combine all posts into a single content, joining with newlines
const combinedText = posts.map(p => p.text).join('\n\n');

const content = {
type: "md",
text: combinedText,
};

const transaction = await NearSocialClient.set({
data: {
[accountId]: {
Expand Down
28 changes: 26 additions & 2 deletions src/services/twitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,39 @@ export class TwitterService {
return tempClient.login(oauthVerifier);
}

async tweet(accessToken, accessSecret, text) {
async tweet(accessToken, accessSecret, posts) {
const userClient = new TwitterApi({
appKey: this.apiKey,
appSecret: this.apiSecret,
accessToken,
accessSecret,
});

return userClient.v2.tweet(text);
// Handle array of post objects
if (!Array.isArray(posts)) {
throw new Error('Posts must be an array');
}

if (posts.length === 1) {
// Single tweet
return userClient.v2.tweet(posts[0].text);
} else {
// Thread implementation
let lastTweetId = null;
const responses = [];

for (const post of posts) {
const tweetData = lastTweetId
? { text: post.text, reply: { in_reply_to_tweet_id: lastTweetId } }
: { text: post.text };

const response = await userClient.v2.tweet(tweetData);
responses.push(response);
lastTweetId = response.data.id;
}

return responses;
}
}

async getUserInfo(accessToken, accessSecret) {
Expand Down

0 comments on commit 0e0adc0

Please sign in to comment.