commit 3079d8e2708066c2f3f1ad79abe77c7c41c38879 Author: Sami Ahmed Date: Sun Jun 7 01:25:14 2026 -0700 v1.6.8: public composer, beep wordmark on posts diff --git a/README.md b/README.md new file mode 100755 index 0000000..4a1861f --- /dev/null +++ b/README.md @@ -0,0 +1,114 @@ +# Beep + +A Twitter/X-style microblog for WordPress. Logged-in users post short updates with rich media, polls, GIFs, voice notes, and quotes. Like, reply (with nested threads), and share beeps. Designed to look like a Twitter timeline, hosted entirely on your own site. + +## What's in v1.5.1 + +### Core Features +- **Post beeps.** Short updates up to 280 characters, with a composer at the top of the feed. +- **Like beeps and replies.** Hearts on every post and every reply, with a pop animation when you tap. +- **Nested replies.** Reply to a reply, to a reply, to a reply. Visual indent caps at two levels (deeper threads still nest structurally). +- **Latest / Top tabs.** "Top" sorts your feed by like count — a built-in way for readers to surface the best stuff. +- **Per-post Beep badge.** Every beep and reply carries a small bird icon in the corner, branding it as a Beep. +- **Friendlier visitor experience.** Logged-out visitors see clean read-only beeps; logged-in users get the full interactive feed. + +### Rich Media +- **Image uploads.** Attach up to 4 images per beep. Previews in the composer with remove buttons. Images display in an adaptive grid (1×, 2×, 3×, 4× layouts). +- **GIF picker (Giphy).** Search and embed GIFs via the Giphy API. Configure your API key in **Settings → Beep**. +- **Voice notes.** Record and attach audio notes (MP3, OGG, WAV, WebM, M4A) up to 20 MB. Inline audio player with play button and waveform visualization. +- **Quote beeps.** Reference any existing beep as a quote. Loads the quoted post's author, time, and content inline. + +### Polls +- **Create polls.** Add a poll to any beep with a question and 2–6 options. +- **Vote on polls.** One-click voting with live results displayed as progress bars. +- **Poll management.** Add/remove options before posting; polls are cleared after submission. + +### Thread View +- **Conversation modal.** Click any beep to open a full thread/conversation view in a modal overlay. +- **Nested thread display.** Replies are indented by depth for easy reading. + +### Settings & Integration +- **Admin settings page.** Configure character limit, Giphy API key, and other options under **Settings → Beep**. +- **Elementor widget.** Drag-and-drop Beep feed into any Elementor-built page. +- **Magic login support.** Works with passwordless login plugins for frictionless onboarding. + +### Moderation +- **Delete any beep or reply.** Logged-in admins see a small × in the top-right of each item. +- **Approve replies before they appear.** Settings → Discussion → "Comment must be manually approved" puts replies in a queue at **Comments** in the sidebar. +- **Suspend users.** Users → click their name → scroll to **Chirp moderation** → check **Suspend from beeping**. + +## Install Beep + +1. In wp-admin, go to **Plugins → Add New → Upload Plugin**. +2. Upload `beep.zip` and click **Install Now**, then **Activate**. +3. On the page you want the feed (your one-page site, a dedicated "Feed" page, etc.), add the shortcode: + + ``` + [beep_feed] + ``` + + Save and visit the page. As an admin, you'll see a composer at the top — type a message and click "Beep" to post. + +## Invite friends (with magic links — no passwords) + +This is the easiest way to let a small circle into your Beep feed without setting up Google/Facebook developer apps. The flow: + +1. **Open registration.** Go to **Settings → General**, check **Anyone can register**, and set **New User Default Role** to **Subscriber**. Save. +2. **Install passwordless login.** Plugins → Add New → search for **Passwordless Login** (any well-reviewed one — Cozmoslabs or Magic Login both work). Install and activate. +3. **Share the sign-up link.** Send your friends a direct link to: + + ``` + https://your-site.com/wp-login.php?action=register + ``` + + They type their email, get a one-time sign-in link, and they're in. No password to remember. +4. **They can now like and reply.** Liking and replying require being logged in; your feed remains read-only for the public. + +## Shortcode options + +``` +[beep_feed limit="50" header="Messages from" sort="latest"] +``` + +- `limit` — how many beeps to load on first render (default 50) +- `header` — text shown next to the wordmark (default "Messages from") +- `sort` — initial sort: `latest` (default) or `top` + +## Elementor Widget + +After activating Beep, you'll find a **Beep Feed** widget in the Elementor panel. Drag it onto your page — it renders the same feed as the shortcode, with no extra configuration needed. + +## Branding + +Swap the bird/wordmark in `assets/`. The accent color is `#1D9BF0`; find/replace in `assets/beep.css` for a different palette. + +## Changelog + +### v1.5.1 +- Fixed critical IIFE scope bug — all functions now properly access `config` and `handleResponse` +- Removed leaked Giphy API key from JavaScript source +- Fixed voice note upload not clearing composer state +- Added CSS for GIF picker modal, poll compose section, media preview grid, voice note preview +- Added loading spinner and empty states for feed +- Added responsive styles for GIF grid and media preview on mobile + +### v1.5 +- Image uploads with 4-image grid +- GIF picker via Giphy API +- Voice note attachments +- Quote beeps +- Polls with voting +- Thread/conversation modal +- Elementor widget +- Admin settings page + +### v1.4 +- Like beeps and replies with pop animation +- Nested replies (multiple levels deep) +- Latest / Top tabs +- Per-post Beep badge +- Read-only mode for logged-out visitors + +## License + +GPLv2 or later, same as WordPress. diff --git a/assets/beep-bird-badge.png b/assets/beep-bird-badge.png new file mode 100755 index 0000000..00c286e Binary files /dev/null and b/assets/beep-bird-badge.png differ diff --git a/assets/beep-bird.png b/assets/beep-bird.png new file mode 100755 index 0000000..c5a27d6 Binary files /dev/null and b/assets/beep-bird.png differ diff --git a/assets/beep-icon-small.png b/assets/beep-icon-small.png new file mode 100755 index 0000000..56a4c45 Binary files /dev/null and b/assets/beep-icon-small.png differ diff --git a/assets/beep-icon.png b/assets/beep-icon.png new file mode 100755 index 0000000..269c34b Binary files /dev/null and b/assets/beep-icon.png differ diff --git a/assets/beep-icon.svg b/assets/beep-icon.svg new file mode 100755 index 0000000..28b2eb2 --- /dev/null +++ b/assets/beep-icon.svg @@ -0,0 +1 @@ +AABCHWp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAAEH3anVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTo4MGE1OWIzZC01OGVjLTRjMGQtOGZlOS0wOGE5MDU2OGM0MDQAAAAB5Gp1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAAO5qdW1iAAAAQWp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuYWN0aW9ucy52MgAAAAAYYzJzaK8iyp4YHAycjjlIX9Ee9rYAAAClY2JvcqFnYWN0aW9uc4GjZmFjdGlvbmxjMnBhLmNyZWF0ZWRtc29mdHdhcmVBZ2VudGhDYW52YSBBSXFkaWdpdGFsU291cmNlVHlwZXhTaHR0cDovL2N2LmlwdGMub3JnL25ld3Njb2Rlcy9kaWdpdGFsc291cmNldHlwZS9jb21wb3NpdGVXaXRoVHJhaW5lZEFsZ29yaXRobWljTWVkaWEAAADFanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaAhdhKaZhalgTl+LaOUx7zkAAAB9Y2JvcqVqZXhjbHVzaW9uc4GiZXN0YXJ0GQEPZmxlbmd0aBlYKGRuYW1lbmp1bWJmIG1hbmlmZXN0Y2FsZ2ZzaGEyNTZkaGFzaFggIFCd6gk7Z17bAjGodcXn7W9opkvaQsuCAjqOTmGwESdjcGFkSQAAAAAAAAAAAAAAAgtqdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAAB3GNib3Knamluc3RhbmNlSUR4LHhtcDppaWQ6MjdmNWQyNDYtMWY5NS00MmNlLTgzZjQtZGE3NGUwYTYwYWFhdGNsYWltX2dlbmVyYXRvcl9pbmZvo2RuYW1lZ2MycGEtcnNndmVyc2lvbmUwLjAuMHdvcmcuY29udGVudGF1dGguYzJwYV9yc2UwLjAuMGlzaWduYXR1cmV4TXNlbGYjanVtYmY9L2MycGEvdXJuOmMycGE6ODBhNTliM2QtNThlYy00YzBkLThmZTktMDhhOTA1NjhjNDA0L2MycGEuc2lnbmF0dXJlcmNyZWF0ZWRfYXNzZXJ0aW9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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2WQIAW89+2EaFY4NtWFf49xafNL2ZjVjdVmrgY2jGQaEYoc6NnOLSV/fr+onREooWoo2PCTAmGH/Uk0J2Eu1oTdfruN66h90qN0zfETgNLKOLh7M7GpSiN9GoP8VY5J9nw9OT2TC0vE/Yu5Oun8cSsIe11e8/CUGkPEuZc+AqBFyCPdzO2hOyXVfbEN3vv8E61InLGnKW8kdS2bZHYlg0lVZi0RGzWOt4EOwbLGYo7GbhEGQDaTAfoRaQtw4vx4oLr6YLzEOIy5tZedpZdhKfXLWOa7unpxQPMlL3L+i+RezjuwHCbdSlMVkVAHH2ORphpT9BaCJayZJapm+GoFnLEkG/rgYrEE0Tdh0tnl8SYURbpbZkHu3BBi7dPaxgeFDHeUjpUTXRyhCwWHxbDdg+K8KeEmanV+h9jNrruEuH+Kdbh349enxpmRZ/dHHVAe2Czk775hN9RPJDhND0Wuz+sCpreEXt4VsR/loZWG8LZg556vOGOQetydI55fvLuY1iynRXIa1lslBEzEluQisqmS0nU686zpsyHbrcMpOkUbuVklosyRBGYIrmy8CirZ5TPbZ/yrjjRX2hU7T34Ia1kbbqJB58gF2ylxmOOeWTgvuVjs0mzcXgUUX3mFAKdOyyFw0pNuvjK4WIXtO7XSxVzy4rtqHUUcTQiHfULisFUGc4YEY=Yes \ No newline at end of file diff --git a/assets/beep-logo.png b/assets/beep-logo.png new file mode 100755 index 0000000..2418e8d Binary files /dev/null and b/assets/beep-logo.png differ diff --git a/assets/beep-logo.svg b/assets/beep-logo.svg new file mode 100755 index 0000000..6f3a098 --- /dev/null +++ b/assets/beep-logo.svg @@ -0,0 +1 @@ +AABCHWp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAAEH3anVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTo2MWNjMjE1ZC04ZTZjLTRjMGQtYWEyNS1kNmY0ZTY0OWIxMGQAAAAB5Gp1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAAO5qdW1iAAAAQWp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuYWN0aW9ucy52MgAAAAAYYzJzaDuUJpf8gX3AOI+dKFeIe9cAAAClY2JvcqFnYWN0aW9uc4GjZmFjdGlvbmxjMnBhLmNyZWF0ZWRtc29mdHdhcmVBZ2VudGhDYW52YSBBSXFkaWdpdGFsU291cmNlVHlwZXhTaHR0cDovL2N2LmlwdGMub3JnL25ld3Njb2Rlcy9kaWdpdGFsc291cmNldHlwZS9jb21wb3NpdGVXaXRoVHJhaW5lZEFsZ29yaXRobWljTWVkaWEAAADFanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaHsl4ugwsbssLQOELklP0JkAAAB9Y2JvcqVqZXhjbHVzaW9uc4GiZXN0YXJ0GQEPZmxlbmd0aBlYKGRuYW1lbmp1bWJmIG1hbmlmZXN0Y2FsZ2ZzaGEyNTZkaGFzaFggbSbE/DmoDKtYqNhKADyJkeRdj3A+ABERW5NpKh5mFl5jcGFkSQAAAAAAAAAAAAAAAgtqdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAAB3GNib3Knamluc3RhbmNlSUR4LHhtcDppaWQ6ZjdjNDhiNWUtNTM3NC00NjQ0LWI0ODctN2EwNzcxNjIzYTIzdGNsYWltX2dlbmVyYXRvcl9pbmZvo2RuYW1lZ2MycGEtcnNndmVyc2lvbmUwLjAuMHdvcmcuY29udGVudGF1dGguYzJwYV9yc2UwLjAuMGlzaWduYXR1cmV4TXNlbGYjanVtYmY9L2MycGEvdXJuOmMycGE6NjFjYzIxNWQtOGU2Yy00YzBkLWFhMjUtZDZmNGU2NDliMTBkL2MycGEuc2lnbmF0dXJlcmNyZWF0ZWRfYXNzZXJ0aW9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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2WQIAtuRdGCUbntEbvZIapFlfdXJsXaUCKfgP5iFKizSBmOUEAt/XHChlgMAmHDLHUS9salP8PLTK1+fnpFxBYiWsu8H23yKlqdz+64qzO8CHI2jMY+cdUMFxyolvc53tdV+jwdt4yaktidEj+IEAP31vpykomfe30fSHv9Qi2Eg4a7BQwpW/58q7R0wW/mHE4iQ3pvODT4LiKBzjW6WZHGqBj4Dl8aPtptYa4YpaKQOIAxFSeXF8erNKTorFXH8Z9jGOWjeSO1GqLYz6chI1E+3E38grLN6plqYnlbh/EckgxtZOvUgrdwT8xbpr5i/XHqRJ1o9IbDsRbCNpZZtki6w/7I8t/gVqjGeJbwW0aYxfJSgeuo4gxasOcoBaW6v2NFmOcXFTOwv9UpmUXGudL2IRIKZoHJlpclKn8gkWou89ssoxZFN9v1SuFWbaGfhIt45nBKBqB69i+wSvMBNVbJy8pp8FDwDMdPhsXK4Eujfv5t4Xk/6fFblnqyaNVexVF83/g6594u1LPDt+BzjkwoOsKiFk+N7Tf9KbeU+hK6dDWslGfcr3sI/RHDD/wTwc5fZv6xkksXjb0fAflm5ezuc1q5bhiMjaKHx1EZpIPg3/bPGJxrbRd4FOzib5Bse9t0c3kXCXcPRkHJE8d1tUIrN3/IMJZxgxvHw/SJsZA1ETiCg=Yes \ No newline at end of file diff --git a/assets/beep-wordmark-blue.png b/assets/beep-wordmark-blue.png new file mode 100644 index 0000000..1eb721d Binary files /dev/null and b/assets/beep-wordmark-blue.png differ diff --git a/assets/beep-wordmark.png b/assets/beep-wordmark.png new file mode 100755 index 0000000..018edcd Binary files /dev/null and b/assets/beep-wordmark.png differ diff --git a/assets/beep.css b/assets/beep.css new file mode 100755 index 0000000..0e03e94 --- /dev/null +++ b/assets/beep.css @@ -0,0 +1,1077 @@ +/* ============================================================ + Beep — Twitter/X Dark Mode + Post and feed styling to match Twitter/X perfectly + ============================================================ */ + +.beep-embed .beep-feed, +.beep-embed .beep-feed * { + box-sizing: border-box; +} + +:root { + /* Default Twitter Dark Mode Palette */ + --beep-bg: #15202B; + --beep-card-bg: #192734; + --beep-card-hover: #1C2732; + --beep-text: #E7E9EA; + --beep-muted: #71767B; + --beep-border: #2F3336; + --beep-accent: #1D9BF0; + --beep-accent-hover: #1A8CD8; + --beep-like: #F91880; + --beep-repost: #00BA7C; +} + + +.beep-embed .beep-feed { + /* Twitter Dark Mode Palette */ + + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + max-width: 600px; + margin: 0 auto; + background: var(--beep-bg); + border-left: 1px solid var(--beep-border); + border-right: 1px solid var(--beep-border); + color: var(--beep-text); + font-size: 15px; + line-height: 1.4; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + min-height: 0; +} + +/* Header */ +.beep-embed .beep-feed-header { + padding: 16px 20px; + border-bottom: 1px solid var(--beep-border); + display: flex; + align-items: center; + justify-content: space-between; + position: sticky; + top: 0; + z-index: 10; + background: rgba(21, 32, 43, 0.85); + backdrop-filter: blur(12px); +} + +.beep-embed .beep-feed-header-title { + font-size: 20px; + font-weight: 800; + color: var(--beep-text); + letter-spacing: -0.5px; +} + +/* Nav tabs */ +.beep-embed .beep-top-nav { + display: flex; + border-bottom: 1px solid var(--beep-border); +} + +.beep-embed .beep-nav-tab { + flex: 1; + background: transparent; + border: 0; + padding: 16px 0; + font-family: inherit; + font-size: 15px; + font-weight: 700; + color: var(--beep-muted); + cursor: pointer; + position: relative; + text-align: center; + transition: background-color 0.15s ease, color 0.15s ease; +} + +.beep-embed .beep-nav-tab:hover { + background: var(--beep-card-hover); + color: var(--beep-text); +} + +.beep-embed .beep-nav-tab.is-active { + color: var(--beep-text); +} + +.beep-embed .beep-nav-tab.is-active::after { + content: ''; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 4px; + background: var(--beep-accent); + border-radius: 9999px; +} + +/* Compose area */ +.beep-embed .beep-compose { + display: flex; + gap: 12px; + padding: 16px 20px; + border-bottom: 1px solid var(--beep-border); + background: var(--beep-bg); +} + +.beep-embed .beep-compose-body { + flex: 1; + min-width: 0; +} + +.beep-embed .beep-feed .beep-input { + width: 100% !important; + border: 0 !important; + outline: 0 !important; + resize: none !important; + background: transparent !important; + font-family: inherit !important; + font-size: 18px !important; + line-height: 1.5 !important; + color: var(--beep-text) !important; + padding: 8px 0 !important; + min-height: 52px; + overflow: hidden; + box-shadow: none !important; + caret-color: var(--beep-accent); +} + +.beep-embed .beep-feed .beep-input::placeholder { + color: var(--beep-muted) !important; +} + +.beep-embed .beep-compose-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid var(--beep-border); +} + +.beep-embed .beep-compose-tools { + display: flex; + gap: 4px; +} + +.beep-embed .beep-compose-tool { + background: transparent; + border: 0; + color: var(--beep-accent); + cursor: pointer; + width: 36px; + height: 36px; + border-radius: 9999px; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.15s ease; +} + +.beep-embed .beep-compose-tool:hover { + background: rgba(29, 155, 240, 0.15); +} + +/* Buttons */ +.beep-embed .beep-button { + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--beep-accent); + color: #fff; + border: 0; + border-radius: 9999px; + padding: 8px 20px; + font-weight: 700; + font-size: 15px; + cursor: pointer; + font-family: inherit; + transition: background-color 0.15s ease, transform 0.1s ease; +} + +.beep-embed .beep-button:hover { background: var(--beep-accent-hover); color: #fff; } +.beep-embed .beep-button:active { transform: scale(0.96); } +.beep-embed .beep-button:disabled { opacity: 0.5; cursor: default; } + +/* Follow button */ +.beep-embed .beep-follow-btn { + background: var(--beep-text); + color: var(--beep-bg); + border: 0; + border-radius: 9999px; + padding: 6px 16px; + font-weight: 700; + font-size: 14px; + cursor: pointer; + font-family: inherit; + transition: opacity 0.15s ease; + white-space: nowrap; +} + +.beep-embed .beep-follow-btn:hover { opacity: 0.85; } + +.beep-embed .beep-following-btn { + background: transparent; + color: var(--beep-text); + border: 1px solid var(--beep-border); + border-radius: 9999px; + padding: 6px 16px; + font-weight: 700; + font-size: 14px; + cursor: pointer; + font-family: inherit; + transition: border-color 0.15s ease, color 0.15s ease, background-color 0.15s ease; +} + +.beep-embed .beep-following-btn:hover { + border-color: #F4212E; + color: #F4212E; + background: rgba(244, 33, 46, 0.1); +} + +/* Char counter */ +.beep-embed .beep-char-counter { font-size: 13px; color: var(--beep-muted); font-variant-numeric: tabular-nums; } +.beep-embed .beep-counter-warn { color: #FFD400; } +.beep-embed .beep-counter-bad { color: #F4212E; font-weight: 700; } + +/* Avatars */ +.beep-embed .beep-feed .beep-avatar { + width: 56px !important; + height: 56px !important; + min-width: 56px !important; + max-width: 56px !important; + border-radius: 50% !important; + flex-shrink: 0; + background: var(--beep-border); + object-fit: cover !important; + aspect-ratio: 1 / 1 !important; + display: block !important; + border: 0 !important; + transition: filter 0.15s ease; +} + +.beep-embed .beep-feed .beep-avatar:hover { filter: brightness(0.9); } + +.beep-embed .beep-feed .beep-avatar-sm { + width: 40px !important; + height: 40px !important; + min-width: 40px !important; + max-width: 40px !important; +} + +/* Post list */ +.beep-embed .beep-list { background: var(--beep-bg); } + +.beep-embed .beep-empty { + padding: 60px 20px; + text-align: center; + color: var(--beep-muted); +} + +/* Individual post */ +.beep-embed .beep-post { + display: flex; + gap: 12px; + padding: 16px 20px; + border-bottom: 1px solid var(--beep-border); + background: var(--beep-bg); + transition: background-color 0.15s ease; + cursor: pointer; +} + +.beep-embed .beep-post:hover { background: var(--beep-card-hover); } +.beep-embed .beep-post:last-child { border-bottom: 0; } + +.beep-embed .beep-post-body { flex: 1; min-width: 0; } + +/* Meta line */ +.beep-embed .beep-meta { + display: flex; + align-items: center; + gap: 6px; + font-size: 16px; + line-height: 1.3; + margin-bottom: 4px; + flex-wrap: wrap; +} + +.beep-embed .beep-name { + font-weight: 700; + color: var(--beep-text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.beep-embed .beep-name:hover { text-decoration: underline; cursor: pointer; } + +.beep-embed .beep-handle, +.beep-embed .beep-dot, +.beep-embed .beep-time { color: var(--beep-muted); font-weight: 400; white-space: nowrap; } + +.beep-embed .beep-time { text-decoration: none; } +.beep-embed .beep-time:hover { text-decoration: underline; cursor: pointer; } + +.beep-embed .beep-spacer { flex: 1; } + +/* Verified badge */ +.beep-embed .beep-verified { + width: 18px; + height: 18px; + fill: var(--beep-accent); + flex-shrink: 0; + margin-left: 2px; + vertical-align: text-bottom; +} + +/* Post text */ +.beep-embed .beep-text { + font-size: 15px; + line-height: 1.5; + color: var(--beep-text); + word-wrap: break-word; + overflow-wrap: anywhere; + margin: 0; + white-space: pre-wrap; +} + +.beep-embed .beep-tag, .beep-embed .beep-mention { color: var(--beep-text); text-decoration: none; cursor: pointer; } +.beep-embed .beep-tag:hover, .beep-embed .beep-mention:hover { text-decoration: underline; } + +.beep-embed .beep-link { color: var(--beep-accent); text-decoration: none; } +.beep-embed .beep-link:hover { text-decoration: underline; } + +/* Action bar */ +.beep-embed .beep-actions { + display: flex; + justify-content: space-between; + max-width: 425px; + margin-top: 12px; +} + +.beep-embed .beep-action { + display: flex; + align-items: center; + gap: 6px; + background: transparent; + border: 0; + color: var(--beep-muted); + font-size: 14px; + font-family: inherit; + cursor: pointer; + padding: 8px 10px; + margin: -8px -10px; + border-radius: 9999px; + transition: color 0.15s ease, background-color 0.15s ease; +} + +.beep-embed .beep-action svg, +.beep-embed .beep-action .beep-icon-svg { + width: 22px; + height: 22px; + fill: currentColor; + display: block; + flex-shrink: 0; +} + +.beep-embed .beep-count { + font-variant-numeric: tabular-nums; + min-width: 20px; +} + +/* Reply - blue */ +.beep-embed .beep-action-reply:hover { + color: var(--beep-accent); + background: rgba(29, 155, 240, 0.1); +} + +/* Retweet - green */ +.beep-embed .beep-action-retweet:hover { + color: var(--beep-repost); + background: rgba(0, 186, 124, 0.1); +} + +.beep-embed .beep-action.is-reposted { color: var(--beep-repost); } + +/* Like - pink */ +.beep-embed .beep-action-like:hover { + color: var(--beep-like); + background: rgba(249, 24, 128, 0.1); +} + +.beep-embed .beep-action.is-liked { color: var(--beep-like); } + +/* Bookmark - blue */ +.beep-embed .beep-action-bookmark:hover { + color: var(--beep-accent); + background: rgba(29, 155, 240, 0.1); +} + +.beep-embed .beep-action.is-bookmarked { color: var(--beep-accent); } + +/* Share - blue */ +.beep-embed .beep-action-share:hover { + color: var(--beep-accent); + background: rgba(29, 155, 240, 0.1); +} + +/* Heart pop animation */ +.beep-embed .beep-action-like.beep-just-liked .beep-icon-filled { + animation: beep-heart-pop 0.55s ease; +} + +@keyframes beep-heart-pop { + 0% { transform: scale(0.85); } + 40% { transform: scale(1.35); } + 70% { transform: scale(0.9); } + 100% { transform: scale(1); } +} + +/* Retweet spin animation */ +.beep-embed .beep-action-retweet.beep-just-retweeted svg { + animation: beep-retweet-spin 0.4s ease; +} + +@keyframes beep-retweet-spin { + 0% { transform: rotate(0deg) scale(1); } + 50% { transform: rotate(180deg) scale(1.2); } + 100% { transform: rotate(360deg) scale(1); } +} + +/* Media */ +.beep-embed .beep-media-attachments { + margin-top: 12px; + border-radius: 16px; + overflow: hidden; + border: 1px solid var(--beep-border); +} + +.beep-embed .beep-media-attachments img { + width: 100%; + height: auto; + display: block; + max-height: 400px; + object-fit: cover; +} + +/* Image grid */ +.beep-embed .beep-media-grid { + display: grid; + gap: 2px; + border-radius: 16px; + overflow: hidden; + border: 1px solid var(--beep-border); +} + +.beep-embed .beep-media-grid-2 { grid-template-columns: 1fr 1fr; } +.beep-embed .beep-media-grid-3 .beep-media-grid-item:first-child { grid-column: span 2; } +.beep-embed .beep-media-grid-4 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; } + +.beep-embed .beep-media-grid-item { + overflow: hidden; + aspect-ratio: 16 / 9; +} + +.beep-embed .beep-media-grid-item img { + width: 100%; + height: 100%; + object-fit: cover; + max-height: none; +} + +/* Polls */ +.beep-embed .beep-poll-container { + margin-top: 12px; + border: 1px solid var(--beep-border); + border-radius: 16px; + padding: 16px; + background: transparent; +} + +.beep-embed .beep-poll-question { + font-size: 15px; + font-weight: 700; + color: var(--beep-text); + margin-bottom: 12px; +} + +.beep-embed .beep-poll-options { + display: flex; + flex-direction: column; + gap: 8px; +} + +.beep-embed .beep-poll-option { + border: 1px solid var(--beep-border); + border-radius: 9999px; + padding: 10px 16px; + cursor: pointer; + font-size: 14px; + font-weight: 700; + color: var(--beep-text); + background: transparent; + transition: background-color 0.15s ease, border-color 0.15s ease; + position: relative; + overflow: hidden; +} + +.beep-embed .beep-poll-option:hover { + background: var(--beep-card-hover); + border-color: var(--beep-muted); +} + +.beep-embed .beep-poll-option.is-voted { border-color: var(--beep-accent); } + +.beep-embed .beep-poll-result { + border: 1px solid var(--beep-border); + border-radius: 9999px; + padding: 10px 16px; + font-size: 14px; + font-weight: 700; + color: var(--beep-text); + position: relative; + overflow: hidden; +} + +.beep-embed .beep-poll-result-bar { + position: absolute; + left: 0; + top: 0; + bottom: 0; + background: var(--beep-accent); + opacity: 0.3; + max-width: 100%; +} + +.beep-embed .beep-poll-result.is-voted { border-color: var(--beep-accent); } +.beep-embed .beep-poll-result-text { position: relative; z-index: 1; } +.beep-embed .beep-poll-end { font-size: 13px; color: var(--beep-muted); margin-top: 8px; font-weight: 400; } + +/* Quotes */ +.beep-embed .beep-quoted-container { + margin-top: 12px; + border: 1px solid var(--beep-border); + border-radius: 16px; + padding: 12px 16px; + background: transparent; +} + +.beep-embed .beep-quoted-post { text-decoration: none; color: inherit; } +.beep-embed .beep-quoted-post:hover { text-decoration: none; } + +.beep-embed .beep-quoted-meta { font-size: 13px; color: var(--beep-muted); margin-bottom: 4px; } +.beep-embed .beep-quoted-meta .beep-name { font-weight: 700; } + +.beep-embed .beep-quoted-text { + font-size: 14px; + color: var(--beep-text); + line-height: 1.4; + margin: 0; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} + +/* Voice note */ +.beep-embed .beep-voice-note { + margin-top: 12px; + display: flex; + align-items: center; + gap: 12px; + background: var(--beep-card-hover); + border-radius: 9999px; + padding: 10px 16px; + max-width: 400px; +} + +.beep-embed .beep-voice-play { + background: var(--beep-accent); + border: 0; + border-radius: 50%; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + flex-shrink: 0; + padding: 0; +} + +.beep-embed .beep-voice-play svg { width: 14px; height: 14px; fill: #fff; } + +.beep-embed .beep-voice-waveform { + flex: 1; + height: 24px; + display: flex; + align-items: center; + gap: 2px; +} + +.beep-embed .beep-voice-bar { + width: 3px; + height: 100%; + background: var(--beep-accent); + border-radius: 9999px; + opacity: 0.6; +} + +.beep-embed .beep-voice-duration { font-size: 13px; color: var(--beep-muted); font-variant-numeric: tabular-nums; } + +/* Replies */ +.beep-embed .beep-replies { + margin-top: 8px; + padding-left: 0; + border-left: 2px solid var(--beep-border); + margin-left: 24px; +} + +.beep-embed .beep-reply { + display: flex; + gap: 10px; + padding: 10px 16px; + border-bottom: 1px solid var(--beep-border); + background: var(--beep-bg); + transition: background-color 0.15s ease; +} + +.beep-embed .beep-reply:last-child { border-bottom: 0; } +.beep-embed .beep-reply-body { flex: 1; min-width: 0; } +.beep-embed .beep-reply .beep-text { font-size: 14px; margin-bottom: 0; } +.beep-embed .beep-reply .beep-meta { font-size: 13px; margin-bottom: 0; } +.beep-embed .beep-reply .beep-avatar { width: 36px !important; height: 36px !important; min-width: 36px !important; max-width: 36px !important; } + +/* Reply form */ +.beep-embed .beep-reply-form { + margin-top: 8px; + padding: 12px 16px; + border-top: 1px solid var(--beep-border); + border-bottom: 1px solid var(--beep-border); + display: flex; + gap: 10px; + align-items: flex-start; + background: var(--beep-bg); +} + +.beep-embed .beep-reply-form[hidden] { display: none; } +.beep-embed .beep-reply-form-body { flex: 1; min-width: 0; } + +.beep-embed .beep-feed .beep-reply-input { + width: 100% !important; + border: 0 !important; + outline: 0 !important; + resize: none !important; + background: transparent !important; + font-family: inherit !important; + font-size: 15px !important; + color: var(--beep-text) !important; + padding: 6px 0 !important; + min-height: 36px; + box-shadow: none !important; + caret-color: var(--beep-accent); +} + +.beep-embed .beep-feed .beep-reply-input::placeholder { color: var(--beep-muted) !important; } + +.beep-embed .beep-reply-footer { + display: flex; + justify-content: flex-end; + align-items: center; + margin-top: 8px; +} + +/* Animations */ +.beep-embed .beep-new { animation: beep-slide-in 0.3s ease; } + +@keyframes beep-slide-in { + from { opacity: 0; transform: translateY(-8px); } + to { opacity: 1; transform: translateY(0); } +} + +/* Post badge */ +.beep-embed .beep-post-badge { + width: 24px; + height: 24px; + flex-shrink: 0; + opacity: 0.7; + margin-left: 4px; +} + +.beep-embed .beep-post-badge:hover { opacity: 1; cursor: pointer; } + +/* Loading */ +.beep-embed .beep-loading { opacity: 0.5; pointer-events: none; } + +/* Mobile */ +@media (max-width: 640px) { + .beep-embed .beep-feed { margin: 0; border-left: 0; border-right: 0; } + .beep-embed .beep-post { padding: 12px 16px; } + .beep-embed .beep-actions { max-width: none; } + .beep-embed .beep-compose { padding: 12px 16px; } + .beep-embed .beep-feed-header { padding: 12px 16px; } +} + +/* Delete */ +/* .beep-delete-row replaced by .beep-bottom-bar */ + +.beep-embed .beep-delete-link { + background: transparent; + border: 0; + color: var(--beep-muted); + font-family: inherit; + font-size: 13px; + cursor: pointer; + padding: 4px 8px; + border-radius: 4px; + transition: color 0.15s ease, background-color 0.15s ease; +} + +.beep-embed .beep-delete-link:hover { color: #F4212E; background: rgba(244, 33, 46, 0.1); } + +/* Notice */ +.beep-embed .beep-notice { padding: 14px 20px; border-bottom: 1px solid var(--beep-border); font-size: 14px; } +.beep-embed .beep-notice p { margin: 0; color: var(--beep-muted); } +.beep-embed .beep-notice-banned { color: #F4212E; font-style: italic; } + +/* Header post button */ +.beep-embed .beep-post-btn-header { + background: var(--beep-accent); + color: #fff; + border: 0; + border-radius: 9999px; + padding: 8px 20px; + font-weight: 700; + font-size: 15px; + cursor: pointer; + font-family: inherit; + transition: background-color 0.15s ease, transform 0.1s ease; +} + +.beep-embed .beep-post-btn-header:hover { background: var(--beep-accent-hover); } +.beep-embed .beep-post-btn-header:active { transform: scale(0.96); } + +/* Back link */ +.beep-embed .beep-back-link { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 20px; + border-bottom: 1px solid var(--beep-border); + background: var(--beep-bg); + color: var(--beep-text); + text-decoration: none; + font-weight: 700; + font-size: 19px; + transition: background-color 0.15s ease; +} + +.beep-embed .beep-back-link:hover { background: var(--beep-card-hover); color: var(--beep-text); text-decoration: none; } +.beep-embed .beep-back-link svg { width: 20px; height: 20px; fill: currentColor; } + +/* Thread modal */ +.beep-embed .beep-thread-modal { + position: fixed; + inset: 0; + z-index: 1000; + display: flex; + align-items: flex-start; + justify-content: center; + background: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(4px); +} + +.beep-embed .beep-thread-modal[hidden] { display: none; } + +.beep-embed .beep-thread-content { + background: var(--beep-card-bg); + width: 100%; + max-width: 600px; + max-height: 90vh; + overflow-y: auto; + border-left: 1px solid var(--beep-border); + border-right: 1px solid var(--beep-border); +} + +.beep-embed .beep-thread-header { + display: flex; + align-items: center; + gap: 16px; + padding: 12px 20px; + border-bottom: 1px solid var(--beep-border); + position: sticky; + top: 0; + background: var(--beep-card-bg); + z-index: 1; +} + +.beep-embed .beep-thread-close { + background: transparent; + border: 0; + color: var(--beep-text); + cursor: pointer; + width: 36px; + height: 36px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + transition: background-color 0.15s ease; +} + +.beep-embed .beep-thread-close:hover { background: var(--beep-card-hover); } +.beep-embed .beep-thread-close svg { width: 20px; height: 20px; fill: currentColor; } +.beep-embed .beep-thread-title { font-size: 18px; font-weight: 700; color: var(--beep-text); } + +/* Tooltip */ +.beep-embed .beep-tip { + position: absolute; + margin-left: 8px; + padding: 4px 10px; + background: var(--beep-text); + color: var(--beep-bg); + border-radius: 4px; + font-size: 12px; + white-space: nowrap; + pointer-events: none; + z-index: 100; + animation: beep-tip-fade 1.5s ease forwards; +} + +@keyframes beep-tip-fade { + 0% { opacity: 0; transform: translateY(-4px); } + 20% { opacity: 1; transform: translateY(0); } + 80% { opacity: 1; } + 100% { opacity: 0; } +} + +/* GIF */ +.beep-embed .beep-gif-embed { + margin-top: 12px; + border-radius: 12px; + overflow: hidden; + border: 1px solid var(--beep-border); +} + +.beep-embed .beep-gif-embed img { + width: 100%; + height: auto; + max-height: 400px; + object-fit: cover; + display: block; + margin: 0; + border-radius: 12px; +} + +/* Profile header */ +.beep-embed .beep-profile-header { padding: 16px 20px; border-bottom: 1px solid var(--beep-border); } +.beep-embed .beep-profile-name { font-size: 20px; font-weight: 800; color: var(--beep-text); margin-bottom: 2px; } +.beep-embed .beep-profile-meta { font-size: 14px; color: var(--beep-muted); } +.beep-embed .beep-profile-stats { display: flex; gap: 20px; margin-top: 12px; } +.beep-embed .beep-profile-stat { color: var(--beep-muted); font-size: 14px; cursor: pointer; } +.beep-embed .beep-profile-stat:hover { text-decoration: underline; } +.beep-embed .beep-profile-stat strong { color: var(--beep-text); font-weight: 700; } + +/* Tabs */ +.beep-embed .beep-tabs { + display: flex; + border-bottom: 1px solid var(--beep-border); + background: var(--beep-card-bg); +} + +.beep-embed .beep-tab { + flex: 1; + background: transparent; + border: 0; + padding: 14px 0; + font-family: inherit; + font-size: 15px; + font-weight: 700; + color: var(--beep-muted); + cursor: pointer; + position: relative; + transition: background-color 0.15s ease, color 0.15s ease; +} + +.beep-embed .beep-tab:hover { background: var(--beep-card-hover); color: var(--beep-text); } +.beep-embed .beep-tab.is-active { color: var(--beep-text); } +.beep-embed .beep-tab.is-active::after { + content: ''; + position: absolute; + left: 50%; + bottom: 0; + transform: translateX(-50%); + width: 56px; + height: 4px; + border-radius: 9999px; + background: var(--beep-accent); +} + +/* Focus */ +.beep-embed .beep-feed .beep-input:focus, +.beep-embed .beep-feed .beep-input:focus-visible, +.beep-embed .beep-feed .beep-reply-input:focus, +.beep-embed .beep-feed .beep-reply-input:focus-visible { + outline: 0 !important; + box-shadow: none !important; +} + +.beep-embed .beep-feed .beep-compose:focus-within { background: rgba(29, 155, 240, 0.03); } + +/* Hidden */ +.beep-embed [hidden] { display: none !important; } + + +/* Magic Login Form */ +.beep-embed .beep-magic-login { + padding: 24px; + display: flex; + justify-content: center; +} + +.beep-embed .beep-magic-login-card { + background: #192734; + border-radius: 16px; + padding: 32px; + width: 100%; + max-width: 400px; + text-align: center; +} + +.beep-embed .beep-magic-login-header { + margin-bottom: 24px; +} + +.beep-embed .beep-magic-login-header svg { + margin-bottom: 16px; +} + +.beep-embed .beep-magic-login-header h3 { + color: #E7E9EA; + font-size: 20px; + font-weight: 700; + margin: 0 0 8px 0; +} + +.beep-embed .beep-magic-login-header p { + color: #71767B; + font-size: 14px; + margin: 0; +} + +.beep-embed .beep-magic-login-form { + display: flex; + flex-direction: column; + gap: 12px; +} + +.beep-embed .beep-magic-email { + width: 100%; + padding: 12px 16px; + background: #22303C; + border: 1px solid #333F4D; + border-radius: 8px; + color: #E7E9EA; + font-size: 16px; + box-sizing: border-box; +} + +.beep-embed .beep-magic-email:focus { + outline: none; + border-color: #1D9BF0; +} + +.beep-embed .beep-magic-email::placeholder { + color: #71767B; +} + +.beep-embed .beep-button.beep-magic-submit { + width: 100%; + padding: 12px 24px; + background: #1D9BF0; + color: #fff; + border: none; + border-radius: 8px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; +} + +.beep-embed .beep-button.beep-magic-submit:hover { + background: #1A8CD8; +} + +.beep-embed .beep-button.beep-magic-submit:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.beep-embed .beep-magic-success { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 12px; + background: rgba(0, 186, 124, 0.1); + border-radius: 8px; + color: #00BA7C; + font-size: 14px; +} + +.beep-embed .beep-magic-error { + padding: 12px; + background: rgba(224, 36, 94, 0.1); + border-radius: 8px; + color: #F4212E; + font-size: 14px; + margin-top: 12px; +} + +/* Public feed notice */ +.beep-embed .beep-public-notice { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 12px 16px; + background: #192734; + border-bottom: 1px solid #333F4D; + color: #71767B; + font-size: 14px; +} + +.beep-embed .beep-public-notice a { + color: #1D9BF0; + text-decoration: none; +} + +.beep-embed .beep-public-notice a:hover { + text-decoration: underline; +} + + + + +/* Bottom bar: wordmark left, delete right */ +/* Bottom bar: wordmark left, delete right */ +/* Bottom bar: delete left, wordmark right (blue, large) */ +/* Bottom bar: delete left, wordmark right (blue) */ +/* Bottom bar: delete left, wordmark text right */ +.beep-embed .beep-bottom-bar { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + margin-top: 4px; +} +.beep-embed .beep-wordmark { + font-size: 16px; + font-weight: 800; + color: #0064cd; + opacity: 0.5; + letter-spacing: -0.5px; + transition: opacity 0.15s ease; + user-select: none; +} +.beep-embed .beep-post:hover .beep-wordmark { + opacity: 1; +} diff --git a/assets/beep.js b/assets/beep.js new file mode 100755 index 0000000..25c91e8 --- /dev/null +++ b/assets/beep.js @@ -0,0 +1,1126 @@ +/* Beep — Twitter-style microblog frontend */ +(function () { + 'use strict'; + + var config = window.BeepConfig || {}; + var limit = (config.charLimit || 280) | 0; + + document.addEventListener('DOMContentLoaded', init); + + function init() { + var feeds = document.querySelectorAll('[data-beep-feed]'); + Array.prototype.forEach.call(feeds, initFeed); + } + + var feedState = { offset: 0, loading: false, hasMore: true, currentSort: 'latest' }; + + function initFeed(feed) { + var compose = feed.querySelector('[data-beep-compose]'); + if (compose) initCompose(compose, feed); + initSortTabs(feed); + initMedia(compose, feed); + initInfiniteScroll(feed); + feed.addEventListener('click', onClick); + feed.addEventListener('input', onInput); + feed.addEventListener('keydown', onKeydown); + } + + /* ---------- Infinite scroll ---------- */ + function initInfiniteScroll(feed) { + var sentinel = document.createElement('div'); + sentinel.className = 'beep-scroll-sentinel'; + sentinel.setAttribute('data-beep-scroll-sentinel', ''); + var list = feed.querySelector('[data-beep-list]'); + if (list) list.appendChild(sentinel); + + if (!('IntersectionObserver' in window)) return; + var observer = new IntersectionObserver(function(entries) { + if (entries[0].isIntersecting && !feedState.loading && feedState.hasMore) { + loadMore(feed); + } + }, { rootMargin: '400px' }); + observer.observe(sentinel); + feed._beepScrollObserver = observer; + } + + function loadMore(feed) { + feedState.loading = true; + var sentinel = feed.querySelector('[data-beep-scroll-sentinel]'); + if (sentinel) sentinel.innerHTML = '
Loading...
'; + + fetch(config.restUrl + 'feed?sort=' + encodeURIComponent(feedState.currentSort) + '&offset=' + feedState.offset, { + credentials: 'same-origin', + headers: { 'X-WP-Nonce': config.nonce } + }) + .then(handleResponse) + .then(function(data) { + feedState.hasMore = !!data.has_more; + feedState.offset = data.offset || feedState.offset + 50; + if (sentinel) sentinel.innerHTML = ''; + if (data.html) { + var list = feed.querySelector('[data-beep-list]'); + if (list) { + var tmp = document.createElement('div'); + tmp.innerHTML = data.html; + while (tmp.firstElementChild) { + // Insert before the sentinel + list.insertBefore(tmp.firstElementChild, sentinel); + } + } + } + if (!feedState.hasMore && sentinel) { + sentinel.innerHTML = '
No more beeps
'; + } + }) + .catch(function() { + if (sentinel) sentinel.innerHTML = ''; + }) + .finally(function() { feedState.loading = false; }); + } + + /* ---------- Composer ---------- */ + function initCompose(compose, feed) { + var input = compose.querySelector('[data-beep-input]'); + var counter = compose.querySelector('[data-beep-counter]'); + var button = compose.querySelector('[data-beep-submit]'); + if (!input || !button) return; + + function refresh() { + autosize(input); + var remaining = limit - countChars(input.value); + counter.textContent = remaining; + counter.classList.toggle('beep-counter-warn', remaining < 20 && remaining >= 0); + counter.classList.toggle('beep-counter-bad', remaining < 0); + button.disabled = input.value.trim() === '' || remaining < 0; + } + input.addEventListener('input', refresh); + refresh(); + + button.addEventListener('click', function () { + var content = input.value.trim(); + if (!content) return; + button.disabled = true; + var quotedPost = compose._beepQuotedPost; + var gifUrl = compose._beepGifUrl; + var payload = { content: content }; + if (quotedPost && quotedPost.id) payload.quote_id = quotedPost.id; + if (gifUrl) payload.gif_url = gifUrl; + + fetch(config.restUrl + 'post', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': config.nonce }, + credentials: 'same-origin', + body: JSON.stringify(payload) + }) + .then(handleResponse) + .then(function (data) { + var list = feed.querySelector('[data-beep-list]'); + var empty = list && list.querySelector('.beep-empty'); + if (empty) empty.remove(); + + // Reload feed to get the fully rendered post (includes media, quotes, polls) + reloadFeed(feed, 'recent'); + + input.value = ''; + clearQuotePreview(compose); + if (compose._beepGifUrl) { + compose._beepGifUrl = null; + var gifPrev = compose.querySelector('[data-beep-gif-preview]'); + if (gifPrev) { gifPrev.innerHTML = ''; gifPrev.hidden = true; } + } + refresh(); + + // Upload pending images and voice note to this new beep + var pending = compose._beepPendingImages; + var pendingVoice = compose._beepPendingVoice; + var uploadDone = function () { + reloadFeed(feed, 'latest'); + }; + + var tasks = []; + if (pending && pending.length && data.id) { + tasks.push(uploadPendingImages(data.id, pending, compose)); + } + if (pendingVoice && data.id) { + tasks.push(uploadPendingVoice(data.id, pendingVoice, compose)); + } + // Create poll if poll data exists + if (data.id) { + tasks.push(createPoll(data.id, compose).then(function() { + clearPoll(compose); + })); + } + + if (tasks.length) { + Promise.all(tasks).then(uploadDone); + } else { + // No uploads, just clear poll if present + clearPoll(compose); + } + }) + .catch(function () { + showToast('Could not post. Please try again.', 'error'); + button.disabled = false; + }); + }); + } + + /* ---------- Sort tabs ---------- */ + function initSortTabs(feed) { + var tabs = feed.querySelector('[data-beep-tabs]'); + if (!tabs) return; + tabs.addEventListener('click', function (e) { + var btn = e.target.closest('[data-beep-sort]'); + if (!btn) return; + e.preventDefault(); + var sort = btn.dataset.beepSort; + tabs.querySelectorAll('[data-beep-sort]').forEach(function (b) { + b.classList.toggle('is-active', b === btn); + }); + reloadFeed(feed, sort); + }); + } + + function reloadFeed(feed, sort) { + var list = feed.querySelector('[data-beep-list]'); + if (!list) return; + list.classList.add('beep-loading'); + feedState.offset = 0; + feedState.hasMore = true; + feedState.currentSort = sort; + fetch(config.restUrl + 'feed?sort=' + encodeURIComponent(sort), { + credentials: 'same-origin', + headers: { 'X-WP-Nonce': config.nonce } + }) + .then(handleResponse) + .then(function (data) { + // Remove everything except the sentinel + var sentinel = list.querySelector('[data-beep-scroll-sentinel]'); + list.innerHTML = ''; + if (data.html) { + var tmp = document.createElement('div'); + tmp.innerHTML = data.html; + while (tmp.firstElementChild) { + list.appendChild(tmp.firstElementChild); + } + } else { + list.innerHTML = '
' + emptyIcon() + '

No beeps yet.

Be the first to share something!
'; + } + if (sentinel) list.appendChild(sentinel); + feedState.hasMore = !!data.has_more; + feedState.offset = data.offset || 50; + list.classList.remove('beep-loading'); + }) + .catch(function () { + list.classList.remove('beep-loading'); + }); + } + + function emptyIcon() { + return ''; + } + + /* ---------- Click delegation ---------- */ + function onClick(e) { + var t = e.target; + + var likePost = t.closest('[data-beep-like-post]'); + if (likePost) { e.preventDefault(); if (requireLogin()) handleLike(likePost, 'like/' + likePost.dataset.beepLikePost); return; } + + var likeReply = t.closest('[data-beep-like-reply]'); + if (likeReply) { e.preventDefault(); if (requireLogin()) handleLike(likeReply, 'reply-like/' + likeReply.dataset.beepLikeReply); return; } + + var replyToggle = t.closest('[data-beep-toggle-reply]'); + if (replyToggle) { + e.preventDefault(); + if (!requireLogin()) return; + var container = replyToggle.closest('[data-beep-post], [data-beep-reply]'); + if (!container) return; + var form = container.querySelector(':scope > .beep-post-body > [data-beep-reply-form], :scope > .beep-reply-body > [data-beep-reply-form]'); + if (!form) return; + form.hidden = !form.hidden; + if (!form.hidden) { + var ta = form.querySelector('textarea'); + if (ta) ta.focus(); + } + return; + } + + var replyBtn = t.closest('[data-beep-reply-submit]'); + if (replyBtn) { e.preventDefault(); handleReplySubmit(replyBtn); return; } + + var shareBtn = t.closest('[data-beep-share]'); + if (shareBtn) { e.preventDefault(); handleShare(shareBtn); return; } + + var delPost = t.closest('[data-beep-delete]'); + if (delPost) { + e.preventDefault(); + handleDeleteWithUndo('delete/' + delPost.dataset.beepDelete, '[data-beep-post="' + delPost.dataset.beepDelete + '"]', 'beep'); + return; + } + + var delReply = t.closest('[data-beep-delete-reply]'); + if (delReply) { + e.preventDefault(); + handleDeleteWithUndo('delete-reply/' + delReply.dataset.beepDeleteReply, '[data-beep-reply="' + delReply.dataset.beepDeleteReply + '"]', 'reply'); + return; + } + + // Thread / conversation view + var threadBtn = t.closest('[data-beep-thread]'); + if (threadBtn) { e.preventDefault(); openThread(threadBtn.dataset.beepThread); return; } + + // Thread modal close + var threadClose = t.closest('[data-beep-thread-close]'); + if (threadClose) { e.preventDefault(); closeThread(); return; } + + // Quote button - open composer with quoted beep pre-loaded + var quoteBtn = t.closest('[data-beep-quote]'); + if (quoteBtn) { e.preventDefault(); openComposerWithQuote(quoteBtn.dataset.beepQuote); return; } + + // Quote composer tab toggle + var quoteComposerBtn = t.closest('[data-beep-quote-composer-btn]'); + if (quoteComposerBtn) { + e.preventDefault(); + var compose = quoteComposerBtn.closest('[data-beep-compose]'); + var quoteSection = compose && compose.querySelector('[data-beep-compose-quote]'); + if (quoteSection) quoteSection.hidden = !quoteSection.hidden; + return; + } + + // Quote lookup - Add button + var quoteLookupBtn = t.closest('[data-beep-quote-lookup-btn]'); + if (quoteLookupBtn) { + e.preventDefault(); + var compose = quoteLookupBtn.closest('[data-beep-compose]'); + var input = compose && compose.querySelector('[data-beep-quote-lookup]'); + var query = input ? input.value.trim() : ''; + if (!query) return; + fetch(config.restUrl + 'lookup?q=' + encodeURIComponent(query), { + headers: { 'X-WP-Nonce': config.nonce }, + credentials: 'same-origin' + }) + .then(handleResponse) + .then(function(quoted) { + if (compose) { + compose._beepQuotedPost = { id: quoted.id, data: quoted }; + showQuotePreview(compose, quoted); + } + }) + .catch(function() { showToast('Beep not found.', 'error'); }); + return; + } + + // Quote remove button + var quoteRemoveBtn = t.closest('[data-beep-quote-remove]'); + if (quoteRemoveBtn) { e.preventDefault(); var compose = quoteRemoveBtn.closest('[data-beep-compose]'); clearQuotePreview(compose); return; } + + // GIF picker button + var gifBtn = t.closest('[data-beep-gif-btn]'); + if (gifBtn) { e.preventDefault(); var compose = gifBtn.closest('[data-beep-compose]'); openGifPicker(compose); return; } + + // GIF modal close + var gifClose = t.closest('[data-beep-gif-close]'); + if (gifClose) { e.preventDefault(); closeGifPicker(); return; } + + // GIF search on Enter (handled in keydown) + // GIF search button + var gifSearchBtn = t.closest('[data-beep-gif-search-btn]'); + if (gifSearchBtn) { + e.preventDefault(); + var compose = gifSearchBtn.closest('[data-beep-compose]') || document.querySelector('[data-beep-compose]'); + var input = gifSearchBtn.closest('.beep-gif-search-bar') && gifSearchBtn.closest('.beep-gif-search-bar').querySelector('[data-beep-gif-search]'); + if (input) searchGifs(input.value, compose); + return; + } + + // GIF remove + var gifRemoveBtn = t.closest('[data-beep-gif-remove]'); + if (gifRemoveBtn) { + e.preventDefault(); + var compose = gifRemoveBtn.closest('[data-beep-compose]'); + if (compose) { + compose._beepGifUrl = null; + var preview = compose.querySelector('[data-beep-gif-preview]'); + if (preview) { preview.innerHTML = ''; preview.hidden = true; } + } + return; + } + + // Poll button - toggle poll section + var pollBtn = t.closest('[data-beep-poll-btn]'); + if (pollBtn) { + e.preventDefault(); + var compose = pollBtn.closest('[data-beep-compose]'); + if (compose) openPollSection(compose); + return; + } + + // Poll add option + var pollAddOptionBtn = t.closest('[data-beep-poll-add-option]'); + if (pollAddOptionBtn) { + e.preventDefault(); + var compose = pollAddOptionBtn.closest('[data-beep-compose]'); + if (compose) addPollOption(compose); + return; + } + + // Poll remove option + var pollRemoveOptionBtn = t.closest('[data-beep-poll-remove-option]'); + if (pollRemoveOptionBtn) { + e.preventDefault(); + var compose = pollRemoveOptionBtn.closest('[data-beep-compose]'); + if (compose) removePollOption(compose); + return; + } + + // Poll remove + var pollRemoveBtn = t.closest('[data-beep-poll-remove]'); + if (pollRemoveBtn) { + e.preventDefault(); + var compose = pollRemoveBtn.closest('[data-beep-compose]'); + if (compose) clearPoll(compose); + return; + } + + // Poll vote button + var pollVoteBtn = t.closest('[data-beep-poll-vote]'); + if (pollVoteBtn) { + e.preventDefault(); + if (requireLogin()) handlePollVote(pollVoteBtn); + return; + } + + // Image lightbox - click on post images to view full size + var lightboxImg = t.closest('[data-beep-lightbox]'); + if (lightboxImg) { + e.preventDefault(); + openLightbox(lightboxImg.href || lightboxImg.querySelector('img').src); + return; + } + + // Lightbox close + var lightboxClose = t.closest('.beep-lightbox'); + if (lightboxClose) { + closeLightbox(); + return; + } + } + + function onInput(e) { + var input = e.target.closest('.beep-reply-input, .beep-compose-input'); + if (!input) return; + autosize(input); + var counter = input.parentElement.querySelector('.beep-char-counter'); + var submit = input.parentElement.querySelector('.beep-button'); + if (counter) { + var remaining = limit - countChars(input.value); + counter.textContent = remaining; + counter.classList.toggle('beep-counter-warn', remaining < 20 && remaining >= 0); + counter.classList.toggle('beep-counter-bad', remaining < 0); + if (submit) submit.disabled = input.value.trim() === '' || remaining < 0; + } + } + + function onKeydown(e) { + if (!((e.metaKey || e.ctrlKey) && e.key === 'Enter')) return; + var input = e.target.closest('textarea'); + if (!input) return; + var wrap = input.closest('.beep-compose, .beep-reply-form'); + if (!wrap) return; + var btn = wrap.querySelector('button'); + if (btn && !btn.disabled) btn.click(); + } + + /* ---------- Like (works for both posts and replies) ---------- */ + function handleLike(btn, urlFragment) { + var countEl = btn.querySelector('[data-beep-like-count]'); + var was = btn.classList.contains('is-liked'); + + btn.classList.toggle('is-liked'); + if (!was) { + btn.classList.add('beep-just-liked'); + setTimeout(function () { btn.classList.remove('beep-just-liked'); }, 600); + } + if (countEl) { + var current = parseInt(countEl.textContent, 10) || 0; + var optimistic = current + (was ? -1 : 1); + countEl.textContent = optimistic > 0 ? optimistic : ''; + } + + fetch(config.restUrl + urlFragment, { + method: 'POST', + headers: { 'X-WP-Nonce': config.nonce }, + credentials: 'same-origin' + }) + .then(handleResponse) + .then(function (data) { + if (countEl) countEl.textContent = data.count > 0 ? data.count : ''; + btn.classList.toggle('is-liked', !!data.liked); + }) + .catch(function () { + btn.classList.toggle('is-liked', was); + }); + } + + /* ---------- Reply submit (top-level or nested) ---------- */ + function handleReplySubmit(btn) { + var postId = btn.dataset.beepReplySubmit; + var parent = btn.dataset.beepReplyParent || '0'; + var form = btn.closest('[data-beep-reply-form]'); + var input = form && form.querySelector('textarea'); + if (!input) return; + var content = input.value.trim(); + if (!content) return; + btn.disabled = true; + + fetch(config.restUrl + 'reply/' + postId, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': config.nonce }, + credentials: 'same-origin', + body: JSON.stringify({ content: content, parent: parseInt(parent, 10) || 0 }) + }) + .then(handleResponse) + .then(function (data) { + if (data.pending) { + form.innerHTML = '
Reply submitted, awaiting moderation.
'; + return; + } + // Reload the full feed to get the properly rendered reply + var feed = btn.closest('[data-beep-feed]'); + if (feed) { + reloadFeed(feed, 'latest'); + } + }) + .catch(function () { + showToast('Could not post reply. Please try again.', 'error'); + }) + .finally(function () { btn.disabled = false; }); + } + + /* ---------- Share (copy link) ---------- */ + function handleShare(btn) { + var url = btn.dataset.beepShare; + if (!url || !navigator.clipboard) return; + navigator.clipboard.writeText(url).then(function () { + showToast('Link copied to clipboard', 'success'); + }); + } + + /* ---------- Delete with undo toast ---------- */ + function handleDeleteWithUndo(pathFragment, selector, type) { + var el = document.querySelector(selector); + if (!el) return; + + // Store original state + var parent = el.parentNode; + var nextSibling = el.nextSibling; + + // Hide the element with animation + el.style.transition = 'opacity 0.3s ease, max-height 0.3s ease'; + el.style.opacity = '0'; + el.style.maxHeight = '0'; + el.style.overflow = 'hidden'; + + // Show undo toast + var toast = document.createElement('div'); + toast.className = 'beep-toast beep-toast-undo'; + toast.innerHTML = '' + (type === 'beep' ? 'Beep' : 'Reply') + ' deleted.'; + document.body.appendChild(toast); + + // Trigger animation + requestAnimationFrame(function() { + toast.classList.add('beep-toast-visible'); + }); + + var deleteTimeout = setTimeout(function() { + // Actually delete + fetch(config.restUrl + pathFragment, { + method: 'DELETE', + headers: { 'X-WP-Nonce': config.nonce }, + credentials: 'same-origin' + }) + .then(handleResponse) + .then(function () { + if (el && el.parentNode) el.remove(); + }) + .catch(function () { + // Restore on failure + restoreElement(el, parent, nextSibling); + showToast('Could not delete.', 'error'); + }); + dismissToast(toast); + }, 4000); + + // Undo button + toast.querySelector('.beep-toast-undo-btn').addEventListener('click', function() { + clearTimeout(deleteTimeout); + restoreElement(el, parent, nextSibling); + dismissToast(toast); + }); + } + + function restoreElement(el, parent, nextSibling) { + el.style.transition = ''; + el.style.opacity = ''; + el.style.maxHeight = ''; + el.style.overflow = ''; + if (nextSibling) { + parent.insertBefore(el, nextSibling); + } else { + parent.appendChild(el); + } + } + + function dismissToast(toast) { + toast.classList.remove('beep-toast-visible'); + setTimeout(function() { toast.remove(); }, 300); + } + + /* ---------- Toast notification ---------- */ + function showToast(message, type) { + var existing = document.querySelector('.beep-toast.beep-toast-msg'); + if (existing) existing.remove(); + + var toast = document.createElement('div'); + toast.className = 'beep-toast beep-toast-msg beep-toast-' + (type || 'info'); + toast.textContent = message; + document.body.appendChild(toast); + requestAnimationFrame(function() { + toast.classList.add('beep-toast-visible'); + }); + setTimeout(function() { dismissToast(toast); }, 3000); + } + + /* ---------- Helpers ---------- */ + function requireLogin() { + if (config.isLoggedIn) return true; + if (config.loginUrl) window.location.href = config.loginUrl; + return false; + } + + function autosize(el) { + el.style.height = 'auto'; + el.style.height = el.scrollHeight + 'px'; + } + + function countChars(s) { + return Array.from(s).length; + } + + function escapeHtml(str) { + var div = document.createElement('div'); + div.appendChild(document.createTextNode(str)); + return div.innerHTML; + } + + /* ---------- Media upload (images) ---------- */ + function initMedia(compose, feed) { + var input = compose && compose.querySelector('[data-beep-media-input]'); + var preview = compose && compose.querySelector('[data-beep-media-preview]'); + var counter = compose && compose.querySelector('[data-beep-media-count]'); + var voiceInput = compose && compose.querySelector('[data-beep-voice-input]'); + var voicePreview = compose && compose.querySelector('[data-beep-voice-preview]'); + if (!input && !voiceInput) return; + + var pendingImages = []; + var pendingVoice = null; + + // Voice note input + if (voiceInput) { + voiceInput.addEventListener('change', function (e) { + var files = e.target.files; + if (!files || !files.length) return; + var file = files[0]; + if (file.size > 20 * 1024 * 1024) { + showToast('Voice note must be under 20 MB.', 'error'); + voiceInput.value = ''; + return; + } + var accepted = ['audio/mpeg','audio/ogg','audio/wav','audio/webm','audio/mp4','audio/x-m4a']; + if (accepted.indexOf(file.type) === -1) { + showToast('Only MP3, OGG, WAV, WebM, and M4A audio are allowed.', 'error'); + voiceInput.value = ''; + return; + } + if (pendingVoice) { + showToast('Only one voice note per beep.', 'error'); + voiceInput.value = ''; + return; + } + var reader = new FileReader(); + reader.onload = function (ev) { + var id = 'voice-' + Date.now(); + pendingVoice = { file: file, id: id }; + compose._beepPendingVoice = pendingVoice; + voicePreview.innerHTML = '
Voice note ready
'; + voicePreview.hidden = false; + }; + reader.readAsDataURL(file); + voiceInput.value = ''; + }); + } + + // Voice remove button + if (voicePreview) { + voicePreview.addEventListener('click', function (e) { + var btn = e.target.closest('[data-beep-voice-remove]'); + if (!btn) return; + pendingVoice = null; + if (compose) compose._beepPendingVoice = null; + voicePreview.innerHTML = ''; + voicePreview.hidden = true; + }); + } + + if (input) { + input.addEventListener('change', function (e) { + var files = Array.prototype.slice.call(e.target.files); + if (!files.length) return; + var room = 4 - pendingImages.length; + if (room <= 0) { input.value = ''; return; } + files = files.slice(0, room); + files.forEach(function (file) { + if (file.size > 10 * 1024 * 1024) { + showToast('Image must be under 10 MB: ' + file.name, 'error'); + return; + } + var reader = new FileReader(); + reader.onload = function (ev) { + var url = ev.target.result; + var id = 'pending-' + Date.now() + '-' + Math.random().toString(36).substr(2, 6); + pendingImages.push({ file: file, previewUrl: url, id: id }); + renderPreviews(); + }; + reader.readAsDataURL(file); + }); + input.value = ''; + }); + } + + function renderPreviews() { + if (!pendingImages.length) { + preview.innerHTML = ''; + preview.hidden = true; + if (counter) counter.hidden = true; + return; + } + preview.hidden = false; + if (counter) { + counter.textContent = pendingImages.length + '/4'; + counter.hidden = false; + } + preview.innerHTML = pendingImages.map(function (img) { + return '
' + + 'Preview' + + '' + + '
'; + }).join(''); + } + + if (preview) { + preview.addEventListener('click', function (e) { + var btn = e.target.closest('[data-remove]'); + if (!btn) return; + var id = btn.dataset.remove; + pendingImages = pendingImages.filter(function (img) { return img.id !== id; }); + compose._beepPendingImages = pendingImages; + renderPreviews(); + }); + } + + if (compose) { + compose._beepPendingImages = pendingImages; + compose._beepPendingVoice = pendingVoice; + } + } + + function uploadPendingImages(postId, pendingImages, compose) { + if (!pendingImages || !pendingImages.length) return Promise.resolve(); + var promises = pendingImages.map(function (img) { + var formData = new FormData(); + formData.append('file', img.file); + return fetch(config.restUrl + 'media/' + postId, { + method: 'POST', + headers: { 'X-WP-Nonce': config.nonce }, + credentials: 'same-origin', + body: formData + }) + .then(handleResponse) + .catch(function (err) { + console.warn('Image upload failed:', img.file.name, err); + }); + }); + return Promise.all(promises).then(function () { + pendingImages.length = 0; + if (compose) { + var preview = compose.querySelector('[data-beep-media-preview]'); + if (preview) { preview.innerHTML = ''; preview.hidden = true; } + var counter = compose.querySelector('[data-beep-media-count]'); + if (counter) counter.hidden = true; + } + }); + } + + function uploadPendingVoice(postId, pendingVoice, compose) { + if (!pendingVoice) return Promise.resolve(); + var formData = new FormData(); + formData.append('file', pendingVoice.file); + return fetch(config.restUrl + 'voice/' + postId, { + method: 'POST', + headers: { 'X-WP-Nonce': config.nonce }, + credentials: 'same-origin', + body: formData + }) + .then(handleResponse) + .then(function () { + if (compose) { + compose._beepPendingVoice = null; + var vp = compose.querySelector('[data-beep-voice-preview]'); + if (vp) { vp.innerHTML = ''; vp.hidden = true; } + } + }) + .catch(function (err) { + console.warn('Voice upload failed:', err); + }); + } + + /* ---------- Thread / Conversation modal ---------- */ + function openThread(postId) { + var modal = document.querySelector('[data-beep-thread-modal]'); + var content = modal && modal.querySelector('[data-beep-thread-content]'); + var loading = modal && modal.querySelector('.beep-thread-loading'); + if (!modal || !content) return; + modal.hidden = false; + if (loading) loading.style.display = 'block'; + content.innerHTML = ''; + + fetch(config.restUrl + 'thread/' + postId, { + headers: { 'X-WP-Nonce': config.nonce }, + credentials: 'same-origin' + }) + .then(handleResponse) + .then(function(data) { + if (loading) loading.style.display = 'none'; + // data can be an array directly or { thread: [...] } + var thread = Array.isArray(data) ? data : (data.thread || []); + renderThread(thread, content); + }) + .catch(function() { + if (loading) loading.textContent = 'Failed to load conversation.'; + }); + } + + function closeThread() { + var modal = document.querySelector('[data-beep-thread-modal]'); + if (modal) modal.hidden = true; + } + + function renderThread(thread, container) { + if (!thread || !thread.length) { + container.innerHTML = '

No replies yet.

'; + return; + } + var html = ''; + thread.forEach(function(item) { + var indent = Math.min(item.depth, 5) * 24; + var author = item.author || {}; + var avatar = escapeHtml(author.avatar || 'https://www.gravatar.com/avatar/?s=48&d=mp'); + var name = escapeHtml(author.name || 'Unknown'); + var username = escapeHtml(author.username || ''); + var content_text = escapeHtml(item.content || ''); + var time_ago = escapeHtml(item.time_ago || ''); + var replyClass = item.depth > 0 ? 'beep-thread-reply' : 'beep-thread-root'; + html += '
'; + html += '
' + name + '
'; + html += '
'; + html += '
'; + html += '' + name + ''; + html += '@' + username + ''; + html += '' + time_ago + ''; + html += '
'; + html += '
' + content_text + '
'; + html += '
'; + html += '' + (item.reply_count || 0) + ' replies'; + html += '' + (item.like_count || 0) + ' likes'; + html += '
'; + }); + container.innerHTML = html; + } + + /* ---------- Quote posts ---------- */ + function openComposerWithQuote(quotedId) { + var compose = document.querySelector('[data-beep-compose]'); + if (!compose) return; + var form = compose.querySelector('.beep-compose-body'); + if (form) form.style.display = 'block'; + compose.scrollIntoView({ behavior: 'smooth', block: 'center' }); + + fetch(config.restUrl + 'quote/' + quotedId, { + headers: { 'X-WP-Nonce': config.nonce }, + credentials: 'same-origin' + }) + .then(handleResponse) + .then(function(quoted) { + compose._beepQuotedPost = { id: quotedId, data: quoted }; + showQuotePreview(compose, quoted); + }) + .catch(function(err) { console.warn('Could not load quoted beep:', err); }); + } + + function showQuotePreview(compose, quoted) { + var preview = compose && compose.querySelector('[data-beep-quote-preview]'); + var removeBtn = compose && compose.querySelector('[data-beep-quote-remove]'); + if (!preview || !quoted) return; + var author = quoted.author || {}; + var avatar = escapeHtml(author.avatar || 'https://www.gravatar.com/avatar/?s=48&d=mp'); + var name = escapeHtml(author.name || 'Unknown'); + var username = escapeHtml(author.username || ''); + var time_ago = escapeHtml(quoted.time_ago || ''); + var content = escapeHtml(quoted.content || ''); + preview.innerHTML = '
' + + '
' + name + '
' + + '
' + + '
' + name + ' ' + + '@' + username + ' ' + + '· ' + + '' + time_ago + '
' + + '
' + content + '
'; + preview.hidden = false; + if (removeBtn) removeBtn.hidden = false; + } + + function clearQuotePreview(compose) { + if (!compose) return; + compose._beepQuotedPost = null; + var preview = compose.querySelector('[data-beep-quote-preview]'); + var removeBtn = compose.querySelector('[data-beep-quote-remove]'); + if (preview) { preview.innerHTML = ''; preview.hidden = true; } + if (removeBtn) removeBtn.hidden = true; + } + + /* ---------- GIF Picker (Giphy) ---------- */ + function openGifPicker(compose) { + var modal = document.querySelector('[data-beep-gif-modal]'); + if (!modal) return; + modal.hidden = false; + var searchInput = modal.querySelector('[data-beep-gif-search]'); + if (searchInput) searchInput.value = ''; + var grid = modal.querySelector('[data-beep-gif-grid]'); + if (grid) grid.innerHTML = '

Type to search GIFs...

'; + if (searchInput) searchInput.focus(); + } + + function closeGifPicker() { + var modal = document.querySelector('[data-beep-gif-modal]'); + if (modal) modal.hidden = true; + } + + function searchGifs(query, compose) { + if (!query || !query.trim()) return; + var apiKey = config.giphyKey || ''; + if (!apiKey) { + console.warn('Beep: No Giphy API key configured.'); + return; + } + var modal = document.querySelector('[data-beep-gif-modal]'); + var grid = modal && modal.querySelector('[data-beep-gif-grid]'); + if (!grid) return; + grid.innerHTML = '

Searching...

'; + + var url = 'https://api.giphy.com/v1/gifs/search?api_key=' + encodeURIComponent(apiKey) + '&q=' + encodeURIComponent(query) + '&limit=30&rating=g'; + + fetch(url) + .then(function(res) { return res.json(); }) + .then(function(data) { + if (!data.data || !data.data.length) { + grid.innerHTML = '

No GIFs found.

'; + return; + } + grid.innerHTML = ''; + data.data.forEach(function(gif) { + var imgUrl = gif.images && gif.images.fixed_width_small && gif.images.fixed_width_small.url; + if (!imgUrl) return; + var item = document.createElement('div'); + item.className = 'beep-gif-item'; + var altText = escapeHtml(gif.title || ''); + item.innerHTML = '' + altText + ''; + item.addEventListener('click', function() { + selectGif(gif, compose); + }); + grid.appendChild(item); + }); + }) + .catch(function() { + grid.innerHTML = '

Search failed.

'; + }); + } + + function selectGif(gif, compose) { + if (!compose) return; + var url = gif.images && gif.images.fixed_width && gif.images.fixed_width.url; + if (!url) return; + compose._beepGifUrl = url; + var preview = compose.querySelector('[data-beep-gif-preview]'); + if (preview) { + preview.innerHTML = '
GIF
'; + preview.hidden = false; + } + closeGifPicker(); + } + + /* ---------- Poll helpers ---------- */ + function openPollSection(compose) { + var pollSection = compose.querySelector('[data-beep-compose-poll]'); + if (!pollSection) return; + pollSection.hidden = false; + var questionInput = pollSection.querySelector('[data-beep-poll-question]'); + if (questionInput) questionInput.focus(); + var options = pollSection.querySelectorAll('[data-beep-poll-option]'); + if (options.length < 2) { + while (options.length < 2) { + addPollOption(compose); + options = pollSection.querySelectorAll('[data-beep-poll-option]'); + } + } + } + + function addPollOption(compose) { + var pollSection = compose.querySelector('[data-beep-compose-poll]'); + if (!pollSection) return; + var optionsContainer = pollSection.querySelector('[data-beep-poll-options]'); + if (!optionsContainer) return; + var optionCount = optionsContainer.querySelectorAll('[data-beep-poll-option]').length; + if (optionCount >= 6) return; + var newRow = document.createElement('div'); + newRow.className = 'beep-poll-option-row'; + newRow.innerHTML = ''; + optionsContainer.appendChild(newRow); + updatePollButtons(compose); + } + + function removePollOption(compose) { + var pollSection = compose.querySelector('[data-beep-compose-poll]'); + if (!pollSection) return; + var options = pollSection.querySelectorAll('[data-beep-poll-option]'); + if (options.length <= 2) return; + var lastOption = options[options.length - 1]; + if (lastOption) lastOption.parentElement.remove(); + updatePollButtons(compose); + } + + function updatePollButtons(compose) { + var pollSection = compose.querySelector('[data-beep-compose-poll]'); + if (!pollSection) return; + var options = pollSection.querySelectorAll('[data-beep-poll-option]'); + var addBtn = pollSection.querySelector('[data-beep-poll-add-option]'); + var removeBtn = pollSection.querySelector('[data-beep-poll-remove-option]'); + if (addBtn) addBtn.hidden = options.length >= 6; + if (removeBtn) removeBtn.hidden = options.length <= 2; + } + + function clearPoll(compose) { + if (!compose) return; + compose._beepPoll = null; + var pollSection = compose.querySelector('[data-beep-compose-poll]'); + if (pollSection) { + pollSection.hidden = true; + var question = pollSection.querySelector('[data-beep-poll-question]'); + if (question) question.value = ''; + var options = pollSection.querySelectorAll('[data-beep-poll-option]'); + if (options.length > 2) { + for (var i = options.length - 1; i >= 2; i--) { + if (options[i].parentElement) options[i].parentElement.remove(); + } + } + options = pollSection.querySelectorAll('[data-beep-poll-option]'); + for (var j = 0; j < options.length; j++) { + options[j].value = ''; + } + } + } + + function collectPollData(compose) { + if (!compose) return null; + var pollSection = compose.querySelector('[data-beep-compose-poll]'); + if (!pollSection || pollSection.hidden) return null; + var questionInput = pollSection.querySelector('[data-beep-poll-question]'); + var question = questionInput ? questionInput.value.trim() : ''; + if (!question) return null; + var optionInputs = pollSection.querySelectorAll('[data-beep-poll-option]'); + var options = []; + for (var i = 0; i < optionInputs.length; i++) { + var val = optionInputs[i].value.trim(); + if (val) options.push(val); + } + if (options.length < 2) return null; + return { question: question, options: options }; + } + + function createPoll(beepId, compose) { + var pollData = collectPollData(compose); + if (!pollData) return Promise.resolve(); + return fetch(config.restUrl + 'poll/' + beepId, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': config.nonce }, + credentials: 'same-origin', + body: JSON.stringify(pollData) + }) + .then(handleResponse) + .catch(function(err) { + console.warn('Poll creation failed:', err); + }); + } + + function handlePollVote(btn) { + var beepId = btn.dataset.beepPollBeep; + var optionIndex = parseInt(btn.dataset.beepPollVote, 10); + if (!beepId || isNaN(optionIndex)) return; + btn.disabled = true; + fetch(config.restUrl + 'poll/' + beepId + '/vote', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': config.nonce }, + credentials: 'same-origin', + body: JSON.stringify({ option_index: optionIndex }) + }) + .then(handleResponse) + .then(function(data) { + var postEl = btn.closest('[data-beep-post]'); + if (postEl) { + var list = postEl.closest('[data-beep-list]'); + if (list) reloadFeed(list.closest('[data-beep-feed]'), 'latest'); + } + }) + .catch(function() { + btn.disabled = false; + }); + } + + /* ---------- Image Lightbox ---------- */ + function openLightbox(src) { + var existing = document.querySelector('.beep-lightbox'); + if (existing) existing.remove(); + var el = document.createElement('div'); + el.className = 'beep-lightbox'; + el.innerHTML = 'Full size image'; + document.body.appendChild(el); + // Close on click outside image or Escape key + el.addEventListener('click', function(e) { + if (e.target === el || e.target.tagName !== 'IMG') closeLightbox(); + }); + var escHandler = function(e) { + if (e.key === 'Escape') { closeLightbox(); document.removeEventListener('keydown', escHandler); } + }; + document.addEventListener('keydown', escHandler); + } + + function closeLightbox() { + var el = document.querySelector('.beep-lightbox'); + if (el) el.remove(); + } + + function handleResponse(res) { + if (!res.ok) return res.json().then(function (j) { throw j; }, function () { throw res; }); + return res.json(); + } +})(); diff --git a/beep.php b/beep.php new file mode 100755 index 0000000..a2288cd --- /dev/null +++ b/beep.php @@ -0,0 +1,39 @@ +register(new \Elementor\Beep_Elementor_Widget()); + } + }, 5); + + // Admin settings (Giphy API key only — full settings via Beep_Settings) + add_action('admin_menu', [__CLASS__, 'add_admin_menu']); + add_action('admin_init', [__CLASS__, 'register_settings']); + } + + public static function add_admin_menu() { + add_options_page( + 'Beep Settings', + 'Beep', + 'manage_options', + 'beep-settings', + [__CLASS__, 'render_settings_page'] + ); + } + + public static function register_settings() { + register_setting('beep_settings_group', 'beep_giphy_key', [ + 'sanitize_callback' => 'sanitize_text_field', + ]); + add_settings_section('beep_main_section', 'API Configuration', null, 'beep-settings'); + add_settings_field( + 'beep_giphy_key', + 'Giphy API Key', + [__CLASS__, 'giphy_key_field'], + 'beep-settings', + 'beep_main_section', + ['label_for' => 'beep_giphy_key'] + ); + } + + public static function giphy_key_field() { + $key = get_option('beep_giphy_key', ''); + echo ''; + echo '

Get a free API key at developers.giphy.com

'; + } + + public static function render_settings_page() { + if (!current_user_can('manage_options')) return; + echo '

Beep Settings

'; + echo '
'; + settings_fields('beep_settings_group'); + do_settings_sections('beep-settings'); + submit_button(); + echo '
'; + } + + public static function activate() { + Beep_CPT::register(); + Beep_Likes::create_table(); + Beep_Polls::create_tables(); + flush_rewrite_rules(); + } + + public static function deactivate() { + flush_rewrite_rules(); + } + + public static function enqueue_assets() { + wp_enqueue_style( + 'beep', + BEEP_URL . 'assets/beep.css', + [], + BEEP_VERSION + ); + wp_enqueue_script( + 'beep', + BEEP_URL . 'assets/beep.js', + [], + BEEP_VERSION, + true + ); + $giphy_key = get_option('beep_giphy_key', ''); + + wp_localize_script('beep', 'BeepConfig', [ + 'restUrl' => esc_url_raw(rest_url('beep/v1/')), + 'nonce' => wp_create_nonce('wp_rest'), + 'charLimit' => BEEP_CHAR_LIMIT, + 'loginUrl' => esc_url_raw(wp_login_url(is_singular() ? get_permalink() : home_url('/'))), + 'isLoggedIn' => is_user_logged_in(), + 'giphyKey' => $giphy_key, + ]); + } +} diff --git a/includes/class-cpt.php b/includes/class-cpt.php new file mode 100755 index 0000000..bc02a47 --- /dev/null +++ b/includes/class-cpt.php @@ -0,0 +1,48 @@ + [ + 'name' => 'Beeps', + 'singular_name' => 'Beep', + 'add_new' => 'New beep', + 'add_new_item' => 'Add new beep', + 'edit_item' => 'Edit beep', + 'view_item' => 'View beep', + 'all_items' => 'All beeps', + 'menu_name' => 'Beeps', + 'search_items' => 'Search beeps', + 'not_found' => 'No beeps found', + 'not_found_in_trash' => 'No beeps in trash', + ], + 'public' => true, + 'show_in_menu' => true, + 'show_in_rest' => true, + 'menu_icon' => 'data:image/png;base64,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', + 'menu_position' => 5, + 'supports' => ['editor', 'author', 'comments'], + 'has_archive' => false, + 'rewrite' => ['slug' => 'beeps'], + 'capability_type' => 'post', + ]); + } + + public static function force_comments_open($open, $post_id) { + if (get_post_type($post_id) === self::POST_TYPE) { + return true; + } + return $open; + } +} diff --git a/includes/class-elementor-widget.php b/includes/class-elementor-widget.php new file mode 100755 index 0000000..69e91e6 --- /dev/null +++ b/includes/class-elementor-widget.php @@ -0,0 +1,127 @@ +start_controls_section( + 'section_feed', + ['label' => 'Feed Settings'] + ); + + $this->add_control( + 'header', + [ + 'label' => 'Header Title', + 'type' => \Elementor\Controls_Manager::TEXT, + 'default' => 'Beep', + 'description' => 'Text shown in the feed header. Leave empty to show the wordmark logo.', + ] + ); + + $this->add_control( + 'show_wordmark', + [ + 'label' => 'Show Wordmark Logo', + 'type' => \Elementor\Controls_Manager::SWITCHER, + 'default' => 'yes', + ] + ); + + $this->add_control( + 'sort', + [ + 'label' => 'Default Sort', + 'type' => \Elementor\Controls_Manager::SELECT, + 'default' => 'latest', + 'options' => [ + 'latest' => 'Latest First', + 'top' => 'Top / Most Liked', + ], + ] + ); + + $this->add_control( + 'limit', + [ + 'label' => 'Posts Per Page', + 'type' => \Elementor\Controls_Manager::NUMBER, + 'min' => 1, + 'max' => 100, + 'default' => 20, + ] + ); + + $this->end_controls_section(); + + $this->start_controls_section( + 'section_style', + ['label' => 'Appearance'] + ); + + $this->add_group_control( + \Elementor\Group_Control_Typography::get_type(), + [ + 'name' => 'feed_typography', + 'label' => 'Feed Typography', + 'selector' => '{{WRAPPER}} .beep-feed', + ] + ); + + $this->add_control( + 'card_radius', + [ + 'label' => 'Card Border Radius', + 'type' => \Elementor\Controls_Manager::SLIDER, + 'size_units' => ['px'], + 'range' => ['px' => ['min' => 0, 'max' => 32]], + 'default' => ['unit' => 'px', 'size' => 0], + 'selectors' => [ + '{{WRAPPER}} .beep-post' => 'border-radius: {{SIZE}}{{UNIT}};', + ], + ] + ); + + $this->end_controls_section(); + } + + protected function render() { + $settings = $this->get_settings_for_display(); + + $atts = [ + 'header' => $settings['header'] ?: 'Beep', + 'show_wordmark' => $settings['show_wordmark'] ? '1' : '0', + 'sort' => $settings['sort'], + 'limit' => (int) $settings['limit'], + ]; + + echo Beep_Shortcode::render_beep_feed($atts); + } +} \ No newline at end of file diff --git a/includes/class-likes.php b/includes/class-likes.php new file mode 100755 index 0000000..572ec0c --- /dev/null +++ b/includes/class-likes.php @@ -0,0 +1,144 @@ +prefix . BEEP_LIKES_TABLE; + } + + /** + * Create or migrate the likes table. + * The 'post_id' column historically only stored beep IDs; we now also + * use it for comment IDs (replies). 'object_type' distinguishes them. + */ + public static function create_table() { + global $wpdb; + $table = self::table_name(); + $charset = $wpdb->get_charset_collate(); + + $sql = "CREATE TABLE $table ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + object_type VARCHAR(20) NOT NULL DEFAULT 'post', + post_id BIGINT(20) UNSIGNED NOT NULL, + user_id BIGINT(20) UNSIGNED NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY object_user (object_type, post_id, user_id), + KEY post_id (post_id), + KEY user_id (user_id) + ) $charset;"; + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + dbDelta($sql); + + // Migration for installs from v1.0-1.3: add object_type column if missing. + $col = $wpdb->get_var($wpdb->prepare( + "SHOW COLUMNS FROM `$table` LIKE %s", 'object_type' + )); + if (!$col) { + $wpdb->query("ALTER TABLE `$table` ADD COLUMN object_type VARCHAR(20) NOT NULL DEFAULT 'post' AFTER id"); + } + } + + public static function register_routes() { + // Like a beep (post). + register_rest_route('beep/v1', '/like/(?P\d+)', [ + 'methods' => 'POST', + 'callback' => [__CLASS__, 'toggle_post_like'], + 'permission_callback' => [__CLASS__, 'can_interact'], + ]); + + // Like a reply (comment). + register_rest_route('beep/v1', '/reply-like/(?P\d+)', [ + 'methods' => 'POST', + 'callback' => [__CLASS__, 'toggle_reply_like'], + 'permission_callback' => [__CLASS__, 'can_interact'], + ]); + } + + public static function can_interact() { + return is_user_logged_in() && !beep_user_is_banned(get_current_user_id()); + } + + public static function toggle_post_like($request) { + $id = (int) $request['id']; + if (get_post_type($id) !== Beep_CPT::POST_TYPE) { + return new WP_Error('beep_not_a_beep', 'Not a beep.', ['status' => 400]); + } + return rest_ensure_response(self::toggle($id, 'post')); + } + + public static function toggle_reply_like($request) { + $id = (int) $request['id']; + $c = get_comment($id); + if (!$c || get_post_type($c->comment_post_ID) !== Beep_CPT::POST_TYPE) { + return new WP_Error('beep_not_a_reply', 'Not a reply.', ['status' => 400]); + } + return rest_ensure_response(self::toggle($id, 'reply')); + } + + /** + * Toggle a like for current user on the given object. + * @return array {liked: bool, count: int} + */ + public static function toggle($object_id, $object_type) { + global $wpdb; + $table = self::table_name(); + $user_id = get_current_user_id(); + + $existing = $wpdb->get_var($wpdb->prepare( + "SELECT id FROM $table WHERE object_type = %s AND post_id = %d AND user_id = %d", + $object_type, $object_id, $user_id + )); + + if ($existing) { + $wpdb->delete($table, ['id' => $existing], ['%d']); + $liked = false; + } else { + $wpdb->insert($table, [ + 'object_type' => $object_type, + 'post_id' => $object_id, + 'user_id' => $user_id, + 'created_at' => current_time('mysql'), + ], ['%s', '%d', '%d', '%s']); + $liked = true; + } + + return [ + 'liked' => $liked, + 'count' => self::get_count($object_id, $object_type), + ]; + } + + public static function get_count($object_id, $object_type = 'post') { + global $wpdb; + $table = self::table_name(); + return (int) $wpdb->get_var($wpdb->prepare( + "SELECT COUNT(*) FROM $table WHERE object_type = %s AND post_id = %d", + $object_type, $object_id + )); + } + + public static function user_has_liked($object_id, $object_type = 'post', $user_id = null) { + global $wpdb; + if (!$user_id) { + $user_id = get_current_user_id(); + } + if (!$user_id) { + return false; + } + $table = self::table_name(); + return (bool) $wpdb->get_var($wpdb->prepare( + "SELECT id FROM $table WHERE object_type = %s AND post_id = %d AND user_id = %d", + $object_type, $object_id, $user_id + )); + } +} diff --git a/includes/class-media.php b/includes/class-media.php new file mode 100755 index 0000000..f2f97d3 --- /dev/null +++ b/includes/class-media.php @@ -0,0 +1,395 @@ + self::$max_file_size) { + return new WP_Error('file_too_large', 'File exceeds 10 MB limit.'); + } + $mime = mime_content_type($file['tmp_name']); + if (!in_array($mime, self::$image_types, true)) { + return new WP_Error('invalid_type', 'Only JPEG, PNG, GIF, and WebP are allowed.'); + } + + $existing = self::get_images($post_id); + if (count($existing) >= self::$max_images) { + return new WP_Error('too_many', 'Maximum ' . self::$max_images . ' images per beep.'); + } + + require_once ABSPATH . 'wp-admin/includes/file.php'; + require_once ABSPATH . 'wp-admin/includes/image.php'; + require_once ABSPATH . 'wp-admin/includes/media.php'; + + $file_return = wp_handle_sideload($file, [ + 'test_form' => false, + 'mimes' => [ + 'jpg|jpeg' => 'image/jpeg', + 'png' => 'image/png', + 'gif' => 'image/gif', + 'webp' => 'image/webp', + ], + ]); + + if (isset($file_return['error'])) { + return new WP_Error('sideload_error', $file_return['error']); + } + + $attachment = [ + 'post_mime_type' => $file_return['type'], + 'post_title' => preg_replace('/\.[^.]+$/', '', basename($file_return['file'])), + 'post_content' => '', + 'post_status' => 'inherit', + 'guid' => $file_return['url'], + ]; + + $attachment_id = wp_insert_attachment($attachment, $file_return['file'], $post_id); + if (is_wp_error($attachment_id)) { + return $attachment_id; + } + + $attach_data = wp_generate_attachment_metadata($attachment_id, $file_return['file']); + wp_update_attachment_metadata($attachment_id, $attach_data); + + add_post_meta($post_id, self::META_KEY_IMAGES, $attachment_id); + + return [ + 'id' => $attachment_id, + 'url' => wp_get_attachment_url($attachment_id), + 'thumb' => self::get_image_url($attachment_id, 'thumbnail'), + 'sizes' => self::get_image_sizes($attachment_id), + ]; + } + + /** + * Remove a single image from a beep post. + */ + public static function remove_image($post_id, $attachment_id) { + $ids = self::get_images($post_id); + $ids = array_values(array_filter($ids, fn($id) => (int) $id !== (int) $attachment_id)); + delete_post_meta($post_id, self::META_KEY_IMAGES); + foreach ($ids as $id) { + add_post_meta($post_id, self::META_KEY_IMAGES, $id); + } + wp_delete_attachment($attachment_id, true); + } + + /** + * Get image attachment IDs for a beep post. + */ + public static function get_images($post_id) { + $ids = get_post_meta($post_id, self::META_KEY_IMAGES, false); + return array_filter(array_map('intval', (array) $ids)); + } + + /** + * Get image srcset-ready sizes for an attachment. + */ + public static function get_image_sizes($attachment_id) { + $sizes = []; + foreach (['thumbnail', 'medium', 'large', 'full'] as $size) { + $img = wp_get_attachment_image_src($attachment_id, $size); + if ($img) { + $sizes[$size] = ['url' => $img[0], 'width' => $img[1], 'height' => $img[2]]; + } + } + return $sizes; + } + + /** + * Get image URL at a given size. + */ + public static function get_image_url($attachment_id, $size = 'medium') { + $img = wp_get_attachment_image_src($attachment_id, $size); + return $img ? $img[0] : ''; + } + + /** + * Set a video URL for a beep post. + */ + public static function set_video_url($post_id, $url) { + $url = esc_url_raw($url); + if (!$url) return new WP_Error('invalid_url', 'Invalid video URL.'); + update_post_meta($post_id, self::META_KEY_VIDEO, $url); + return true; + } + + /** + * Get the video URL for a beep post. + */ + public static function get_video_url($post_id) { + return esc_url_raw(get_post_meta($post_id, self::META_KEY_VIDEO, true)); + } + + /** + * Detect if a URL is a supported video oEmbed. + */ + public static function detect_video_url($url) { + $patterns = [ + 'youtube' => '#https?://(?:www\.)?youtube\.com/watch\?v=[^\s&#]+#', + 'yt_s' => '#https?://youtu\.be/[^\s&#]+#', + 'vimeo' => '#https?://vimeo\.com/\d+#', + 'twitter' => '#https?://(?:twitter\.com|x\.com)/[^\s#/]+/status/\d+#', + ]; + foreach ($patterns as $name => $pattern) { + if (preg_match($pattern, $url)) return $name; + } + return false; + } + + /** + * Render the Twitter-style image gallery. + * + * @param int[] $image_ids + * @return string HTML + */ + public static function render_image_gallery($image_ids) { + if (empty($image_ids)) return ''; + + $count = count($image_ids); + $grid_class = 'beep-gallery'; + if ($count === 1) $grid_class .= ' beep-gallery-1'; + elseif ($count === 2) $grid_class .= ' beep-gallery-2'; + elseif ($count === 3) $grid_class .= ' beep-gallery-3'; + else $grid_class .= ' beep-gallery-4'; + + $html = '
'; + foreach ($image_ids as $i => $id) { + $full = self::get_image_url($id, 'large'); + $thumb = self::get_image_url($id, 'medium'); + if (!$thumb) $thumb = $full; + if (!$full) continue; + $alt = get_post_meta($id, '_wp_attachment_image_alt', true); + $html .= ''; + } + $html .= '
'; + return $html; + } + + /** + * Render video embed via oEmbed. + */ + public static function render_video_embed($url) { + if (!$url) return ''; + $embed = wp_oembed_get($url, ['width' => 560, 'height' => 315]); + if ($embed) { + return '
' . $embed . '
'; + } + return ''; + } + + /** + * Delete all media for a beep post (when beep is deleted). + */ + public static function delete_media($post_id) { + foreach (self::get_images($post_id) as $id) { + wp_delete_attachment($id, true); + } + delete_post_meta($post_id, self::META_KEY_IMAGES); + delete_post_meta($post_id, self::META_KEY_VIDEO); + } + // Voice note support + private static $audio_types = ['audio/mpeg', 'audio/ogg', 'audio/wav', 'audio/webm', 'audio/mp4', 'audio/x-m4a']; + private static $max_audio_size = 20 * 1024 * 1024; // 20 MB + + public static function handle_voice_upload($post_id, $file) { + if (!is_array($file) || $file['error'] !== UPLOAD_ERR_OK) { + return new WP_Error('upload_error', 'Upload failed.'); + } + if ($file['size'] > self::$max_audio_size) { + return new WP_Error('file_too_large', 'Voice note exceeds 20 MB limit.'); + } + $mime = mime_content_type($file['tmp_name']); + if (!in_array($mime, self::$audio_types, true)) { + return new WP_Error('invalid_type', 'Only MP3, OGG, WAV, WebM, and M4A audio are allowed.'); + } + + require_once ABSPATH . 'wp-admin/includes/file.php'; + require_once ABSPATH . 'wp-admin/includes/media.php'; + + $file_return = wp_handle_upload($file, [ + 'test_form' => false, + 'mimes' => [ + 'mp3' => 'audio/mpeg', + 'ogg' => 'audio/ogg', + 'wav' => 'audio/wav', + 'webm' => 'audio/webm', + 'm4a' => 'audio/mp4', + ], + ]); + + if (isset($file_return['error'])) { + return new WP_Error('upload_error', $file_return['error']); + } + + $attachment = [ + 'post_mime_type' => $file_return['type'], + 'post_title' => preg_replace('/\.[^.]+$/', '', basename($file_return['file'])), + 'post_content' => '', + 'post_status' => 'inherit', + 'guid' => $file_return['url'], + ]; + + $attachment_id = wp_insert_attachment($attachment, $file_return['file'], $post_id); + if (is_wp_error($attachment_id)) { + return $attachment_id; + } + + $attach_data = wp_generate_attachment_metadata($attachment_id, $file_return['file']); + wp_update_attachment_metadata($attachment_id, $attach_data); + + update_post_meta($post_id, 'beep_voice_url', wp_get_attachment_url($attachment_id)); + update_post_meta($post_id, 'beep_voice_id', $attachment_id); + + return [ + 'id' => $attachment_id, + 'url' => wp_get_attachment_url($attachment_id), + ]; + } + + public static function get_voice_url($post_id) { + return esc_url_raw(get_post_meta($post_id, 'beep_voice_url', true)); + } + + public static function remove_voice($post_id) { + $id = (int) get_post_meta($post_id, 'beep_voice_id', true); + if ($id) { + wp_delete_attachment($id, true); + } + delete_post_meta($post_id, 'beep_voice_url'); + delete_post_meta($post_id, 'beep_voice_id'); + } + + public static function render_voice_note($post_id) { + $url = self::get_voice_url($post_id); + if (!$url) return ''; + $duration = ''; + $meta = get_post_meta($post_id, 'beep_voice_id', true); + if ($meta) { + $metadata = wp_get_attachment_metadata($meta); + if ($metadata && isset($metadata['length_formatted'])) { + $duration = $metadata['length_formatted']; + } + } + $dur_html = $duration ? '' . esc_html($duration) . '' : ''; + return '
' . + '' . + '
Voice note
' . + $dur_html . + '
'; + } + + // Video file upload support + private static $video_types = ['video/mp4', 'video/webm', 'video/ogg', 'video/quicktime']; + private static $max_video_size = 100 * 1024 * 1024; // 100 MB + + public static function handle_video_upload($post_id, $file) { + if (!is_array($file) || $file['error'] !== UPLOAD_ERR_OK) { + return new WP_Error('upload_error', 'Upload failed.'); + } + if ($file['size'] > self::$max_video_size) { + return new WP_Error('file_too_large', 'Video exceeds 100 MB limit.'); + } + $mime = mime_content_type($file['tmp_name']); + if (!in_array($mime, self::$video_types, true)) { + return new WP_Error('invalid_type', 'Only MP4, WebM, OGG, and MOV are allowed.'); + } + + require_once ABSPATH . 'wp-admin/includes/file.php'; + require_once ABSPATH . 'wp-admin/includes/media.php'; + + $file_return = wp_handle_upload($file, [ + 'test_form' => false, + 'mimes' => [ + 'mp4' => 'video/mp4', + 'webm' => 'video/webm', + 'ogg' => 'video/ogg', + 'mov' => 'video/quicktime', + ], + ]); + + if (isset($file_return['error'])) { + return new WP_Error('upload_error', $file_return['error']); + } + + $attachment = [ + 'post_mime_type' => $file_return['type'], + 'post_title' => preg_replace('/\.[^.]+$/', '', basename($file_return['file'])), + 'post_content' => '', + 'post_status' => 'inherit', + 'guid' => $file_return['url'], + ]; + + $attachment_id = wp_insert_attachment($attachment, $file_return['file'], $post_id); + if (is_wp_error($attachment_id)) { + return $attachment_id; + } + + $attach_data = wp_generate_attachment_metadata($attachment_id, $file_return['file']); + wp_update_attachment_metadata($attachment_id, $attach_data); + + update_post_meta($post_id, 'beep_video_url', wp_get_attachment_url($attachment_id)); + update_post_meta($post_id, 'beep_video_id', $attachment_id); + + return [ + 'id' => $attachment_id, + 'url' => wp_get_attachment_url($attachment_id), + ]; + } + + public static function get_upload_video_url($post_id) { + return esc_url_raw(get_post_meta($post_id, 'beep_video_url', true)); + } + + public static function remove_video($post_id) { + $id = (int) get_post_meta($post_id, 'beep_video_id', true); + if ($id) { + wp_delete_attachment($id, true); + } + delete_post_meta($post_id, 'beep_video_url'); + delete_post_meta($post_id, 'beep_video_id'); + } + + public static function render_video_player($post_id) { + $url = self::get_upload_video_url($post_id); + if (!$url) return ''; + return '
' . + '
'; + } + +} \ No newline at end of file diff --git a/includes/class-moderation.php b/includes/class-moderation.php new file mode 100755 index 0000000..e2c8458 --- /dev/null +++ b/includes/class-moderation.php @@ -0,0 +1,50 @@ +ID) { + return; + } + $can_ban = current_user_can('edit_users'); + $banned = get_user_meta($user->ID, self::BAN_META_KEY, true) === '1'; + ?> +

Beep moderation

+ + + + + +
+ + + + + +
+ prefix . self::TABLE_NAME; + } + + public static function votes_table_name() { + global $wpdb; + return $wpdb->prefix . self::VOTES_TABLE; + } + + /** + * Create the polls and poll_votes tables. + */ + public static function create_tables() { + global $wpdb; + $polls = self::table_name(); + $votes = self::votes_table_name(); + $charset = $wpdb->get_charset_collate(); + + $polls_sql = "CREATE TABLE $polls ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + beep_id BIGINT(20) UNSIGNED NOT NULL, + question TEXT NOT NULL, + options TEXT NOT NULL, + votes TEXT NOT NULL DEFAULT '[]', + total_votes BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, + ends_at DATETIME NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + KEY beep_id (beep_id) + ) $charset;"; + + $votes_sql = "CREATE TABLE $votes ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + poll_id BIGINT(20) UNSIGNED NOT NULL, + user_id BIGINT(20) UNSIGNED NOT NULL, + option_index INT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY poll_user (poll_id, user_id), + KEY poll_id (poll_id), + KEY user_id (user_id) + ) $charset;"; + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + dbDelta($polls_sql); + dbDelta($votes_sql); + } + + public static function register_routes() { + // Create a poll for a beep + register_rest_route('beep/v1', '/poll/(?P\d+)', [ + 'methods' => 'POST', + 'callback' => [__CLASS__, 'create_poll'], + 'permission_callback' => [__CLASS__, 'can_create_poll'], + ]); + + // Get poll data for a beep + register_rest_route('beep/v1', '/poll/(?P\d+)', [ + 'methods' => 'GET', + 'callback' => [__CLASS__, 'get_poll'], + 'permission_callback' => '__return_true', + ]); + + // Vote on a poll + register_rest_route('beep/v1', '/poll/(?P\d+)/vote', [ + 'methods' => 'POST', + 'callback' => [__CLASS__, 'vote'], + 'permission_callback' => [__CLASS__, 'can_vote'], + ]); + } + + public static function can_create_poll() { + return is_user_logged_in() && !beep_user_is_banned(get_current_user_id()); + } + + public static function can_vote() { + return is_user_logged_in() && !beep_user_is_banned(get_current_user_id()); + } + + /** + * Create a poll for a beep post. + * Expects JSON body: { "question": "...", "options": ["a", "b", ...], "ends_at": "YYYY-MM-DDTHH:MM:SS" } + */ + public static function create_poll($request) { + $beep_id = (int) $request['beep_id']; + $post = get_post($beep_id); + + if (!$post || $post->post_type !== Beep_CPT::POST_TYPE) { + return new WP_Error('not_found', 'Beep not found.', ['status' => 404]); + } + + if (!current_user_can('edit_post', $beep_id)) { + return new WP_Error('forbidden', 'Cannot create poll for this beep.', ['status' => 403]); + } + + // Check if poll already exists + if (self::poll_exists($beep_id)) { + return new WP_Error('poll_exists', 'A poll already exists for this beep.', ['status' => 400]); + } + + $body = $request->get_json_params(); + $question = sanitize_text_field($body['question'] ?? ''); + $options = $body['options'] ?? []; + $ends_at = isset($body['ends_at']) ? sanitize_text_field($body['ends_at']) : null; + + if (empty($question)) { + return new WP_Error('missing_question', 'Poll question is required.', ['status' => 400]); + } + + if (count($options) < 2) { + return new WP_Error('too_few_options', 'Poll must have at least 2 options.', ['status' => 400]); + } + + if (count($options) > 6) { + return new WP_Error('too_many_options', 'Poll can have at most 6 options.', ['status' => 400]); + } + + // Sanitize options + $options = array_map('sanitize_text_field', $options); + $votes = array_fill(0, count($options), 0); + + global $wpdb; + $table = self::table_name(); + + $result = $wpdb->insert($table, [ + 'beep_id' => $beep_id, + 'question' => $question, + 'options' => json_encode($options), + 'votes' => json_encode($votes), + 'total_votes' => 0, + 'ends_at' => $ends_at ? date('Y-m-d H:i:s', strtotime($ends_at)) : null, + 'created_at' => current_time('mysql'), + ], ['%d', '%s', '%s', '%s', '%d', '%s', '%s']); + + if ($result === false) { + return new WP_Error('db_error', 'Failed to create poll.', ['status' => 500]); + } + + $poll_id = $wpdb->insert_id; + + // Store poll_id in post meta for easy lookup + update_post_meta($beep_id, 'beep_poll_id', $poll_id); + + return rest_ensure_response([ + 'id' => $poll_id, + 'beep_id' => $beep_id, + 'question' => $question, + 'options' => $options, + 'votes' => $votes, + 'total_votes' => 0, + 'ends_at' => $ends_at, + ]); + } + + /** + * Get poll data for a beep. + */ + public static function get_poll($request) { + $beep_id = (int) $request['beep_id']; + $poll = self::get_poll_by_beep_id($beep_id); + + if (!$poll) { + return new WP_Error('not_found', 'Poll not found.', ['status' => 404]); + } + + $user_id = get_current_user_id(); + $user_vote = null; + if ($user_id) { + $user_vote = self::get_user_vote($poll['id'], $user_id); + } + + return rest_ensure_response([ + 'id' => $poll['id'], + 'beep_id' => $poll['beep_id'], + 'question' => $poll['question'], + 'options' => $poll['options'], + 'votes' => $poll['votes'], + 'total_votes' => $poll['total_votes'], + 'ends_at' => $poll['ends_at'], + 'has_ended' => $poll['ends_at'] ? (strtotime($poll['ends_at']) < time()) : false, + 'user_vote' => $user_vote, + ]); + } + + /** + * Vote on a poll. + * Expects JSON body: { "option_index": 0 } + */ + public static function vote($request) { + $beep_id = (int) $request['beep_id']; + $poll = self::get_poll_by_beep_id($beep_id); + + if (!$poll) { + return new WP_Error('not_found', 'Poll not found.', ['status' => 404]); + } + + // Check if poll has ended + if ($poll['ends_at'] && strtotime($poll['ends_at']) < time()) { + return new WP_Error('poll_ended', 'This poll has ended.', ['status' => 400]); + } + + $body = $request->get_json_params(); + $option_index = (int) ($body['option_index'] ?? -1); + + if ($option_index < 0 || $option_index >= count($poll['options'])) { + return new WP_Error('invalid_option', 'Invalid option index.', ['status' => 400]); + } + + $user_id = get_current_user_id(); + + // Check if user already voted + $existing_vote = self::get_user_vote($poll['id'], $user_id); + if ($existing_vote !== null) { + // If same option, un-vote (toggle) + if ($existing_vote === $option_index) { + return rest_ensure_response(self::remove_vote($poll['id'], $user_id, $option_index, $beep_id)); + } + // Different option - change vote + return rest_ensure_response(self::change_vote($poll['id'], $user_id, $existing_vote, $option_index, $beep_id)); + } + + // Cast new vote + return rest_ensure_response(self::cast_vote($poll['id'], $user_id, $option_index, $beep_id)); + } + + private static function cast_vote($poll_id, $user_id, $option_index, $beep_id) { + global $wpdb; + $polls_table = self::table_name(); + $votes_table = self::votes_table_name(); + + // Insert vote + $wpdb->insert($votes_table, [ + 'poll_id' => $poll_id, + 'user_id' => $user_id, + 'option_index' => $option_index, + 'created_at' => current_time('mysql'), + ], ['%d', '%d', '%d', '%s']); + + // Update poll votes and total + $poll = self::get_poll_by_id($poll_id); + $votes = $poll['votes']; + $votes[$option_index]++; + $total_votes = $poll['total_votes'] + 1; + + $wpdb->update( + $polls_table, + ['votes' => json_encode($votes), 'total_votes' => $total_votes], + ['id' => $poll_id], + ['%s', '%d'], + ['%d'] + ); + + return [ + 'voted' => true, + 'option_index' => $option_index, + 'votes' => $votes, + 'total_votes' => $total_votes, + 'user_vote' => $option_index, + ]; + } + + private static function remove_vote($poll_id, $user_id, $option_index, $beep_id) { + global $wpdb; + $polls_table = self::table_name(); + $votes_table = self::votes_table_name(); + + // Delete vote + $wpdb->delete($votes_table, [ + 'poll_id' => $poll_id, + 'user_id' => $user_id, + ], ['%d', '%d']); + + // Update poll votes and total + $poll = self::get_poll_by_id($poll_id); + $votes = $poll['votes']; + $votes[$option_index] = max(0, $votes[$option_index] - 1); + $total_votes = max(0, $poll['total_votes'] - 1); + + $wpdb->update( + $polls_table, + ['votes' => json_encode($votes), 'total_votes' => $total_votes], + ['id' => $poll_id], + ['%s', '%d'], + ['%d'] + ); + + return [ + 'voted' => false, + 'option_index' => null, + 'votes' => $votes, + 'total_votes' => $total_votes, + 'user_vote' => null, + ]; + } + + private static function change_vote($poll_id, $user_id, $old_index, $new_index, $beep_id) { + global $wpdb; + $polls_table = self::table_name(); + $votes_table = self::votes_table_name(); + + // Update vote + $wpdb->update( + $votes_table, + ['option_index' => $new_index], + ['poll_id' => $poll_id, 'user_id' => $user_id], + ['%d'], + ['%d', '%d'] + ); + + // Update poll votes + $poll = self::get_poll_by_id($poll_id); + $votes = $poll['votes']; + $votes[$old_index] = max(0, $votes[$old_index] - 1); + $votes[$new_index]++; + + $wpdb->update( + $polls_table, + ['votes' => json_encode($votes)], + ['id' => $poll_id], + ['%s'], + ['%d'] + ); + + return [ + 'voted' => true, + 'option_index' => $new_index, + 'votes' => $votes, + 'total_votes' => $poll['total_votes'], + 'user_vote' => $new_index, + ]; + } + + /** + * Get poll by beep ID. + */ + public static function get_poll_by_beep_id($beep_id) { + // First check post meta for poll_id + $poll_id = get_post_meta($beep_id, 'beep_poll_id', true); + if ($poll_id) { + return self::get_poll_by_id($poll_id); + } + + // Fallback to table lookup + global $wpdb; + $table = self::table_name(); + $row = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM $table WHERE beep_id = %d", + $beep_id + ), ARRAY_A); + + if (!$row) { + return null; + } + + return self::format_poll($row); + } + + /** + * Get poll by ID. + */ + public static function get_poll_by_id($poll_id) { + global $wpdb; + $table = self::table_name(); + $row = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM $table WHERE id = %d", + $poll_id + ), ARRAY_A); + + if (!$row) { + return null; + } + + return self::format_poll($row); + } + + /** + * Check if poll exists for a beep. + */ + public static function poll_exists($beep_id) { + return self::get_poll_by_beep_id($beep_id) !== null; + } + + /** + * Get user's vote for a poll. + */ + public static function get_user_vote($poll_id, $user_id) { + global $wpdb; + $table = self::votes_table_name(); + $row = $wpdb->get_row($wpdb->prepare( + "SELECT option_index FROM $table WHERE poll_id = %d AND user_id = %d", + $poll_id, $user_id + )); + + return $row ? (int) $row->option_index : null; + } + + /** + * Format poll row from database. + */ + private static function format_poll($row) { + return [ + 'id' => (int) $row['id'], + 'beep_id' => (int) $row['beep_id'], + 'question' => $row['question'], + 'options' => json_decode($row['options'], true), + 'votes' => json_decode($row['votes'], true), + 'total_votes' => (int) $row['total_votes'], + 'ends_at' => $row['ends_at'], + 'created_at' => $row['created_at'], + ]; + } + + /** + * Get poll results with percentages (for display). + */ + public static function get_results_with_percentages($poll) { + $total = $poll['total_votes']; + $results = []; + foreach ($poll['options'] as $i => $option) { + $votes = $poll['votes'][$i] ?? 0; + $percentage = $total > 0 ? round(($votes / $total) * 100) : 0; + $results[] = [ + 'index' => $i, + 'option' => $option, + 'votes' => $votes, + 'percentage' => $percentage, + ]; + } + return $results; + } +} diff --git a/includes/class-quotes.php b/includes/class-quotes.php new file mode 100755 index 0000000..8ceb15d --- /dev/null +++ b/includes/class-quotes.php @@ -0,0 +1,102 @@ +post_type !== Beep_CPT::POST_TYPE) { + return new WP_Error('invalid_quoted', 'Cannot quote that beep.'); + } + update_post_meta($new_post_id, self::QUOTED_META, (int) $quoted_post_id); + return true; + } + + /** + * Get the quoted post ID for a beep. + */ + public static function get_quoted_post_id($post_id) { + return (int) get_post_meta($post_id, self::QUOTED_META, true); + } + + /** + * Get the quoted post data for rendering. + */ + public static function get_quoted_post($post_id) { + $quoted_id = self::get_quoted_post_id($post_id); + if (!$quoted_id) return null; + + $post = get_post($quoted_id); + if (!$post || $post->post_status !== 'publish') return null; + + $author_id = $post->post_author; + $author = get_userdata($author_id); + $avatar = get_user_meta($author_id, 'beep_avatar', true); + if (!$avatar) { + $avatar = 'https://www.gravatar.com/avatar/' . md5(strtolower($author->user_email ?? '')) . '?s=48&d=mp'; + } + + $content = preg_replace('/\[beep:\d+\]/', '', $post->post_content); + + return [ + 'id' => $post->ID, + 'content' => $content, + 'time_ago'=> self::time_ago(strtotime($post->post_date)), + 'author' => [ + 'id' => $author_id, + 'name' => $author->display_name ?? 'Unknown', + 'username' => $author->user_login ?? '', + 'avatar' => $avatar, + ], + ]; + } + + /** + * Render the quoted beep HTML (the embedded original beep). + */ + public static function render_quoted_post($post_id) { + $quoted = self::get_quoted_post($post_id); + if (!$quoted) return ''; + + $author = $quoted['author']; + $avatar = esc_url($author['avatar']); + $name = esc_html($author['name']); + $handle = esc_html('@' . $author['username']); + $content = make_clickable(wp_kses_post($quoted['content'])); + $time = esc_html($quoted['time_ago']); + $qid = esc_attr($quoted['id']); + + return '
' . + '
' . + '' . $name . '' . + '
' . + '
' . + '
' . + '' . $name . '' . + '' . $handle . '' . + '·' . + '' . $time . '' . + '
' . + '
' . $content . '
' . + '
'; + } + + private static function time_ago($timestamp) { + $diff = time() - $timestamp; + if ($diff < 0) $diff = 0; + if ($diff < 60) return $diff . 's'; + if ($diff < 3600) return floor($diff / 60) . 'm'; + if ($diff < 86400) return floor($diff / 3600) . 'h'; + if ($diff < 604800) return floor($diff / 86400) . 'd'; + return gmdate('M j', $timestamp); + } +} diff --git a/includes/class-replies.php b/includes/class-replies.php new file mode 100755 index 0000000..90116cf --- /dev/null +++ b/includes/class-replies.php @@ -0,0 +1,252 @@ + 'POST', + 'callback' => [__CLASS__, 'submit_beep'], + 'permission_callback' => ['Beep_Likes', 'can_interact'], + ]); + register_rest_route('beep/v1', '/reply/(?P\d+)', [ + 'methods' => 'POST', + 'callback' => [__CLASS__, 'submit_reply'], + 'permission_callback' => ['Beep_Likes', 'can_interact'], + ]); + register_rest_route('beep/v1', '/thread/(?P\d+)', [ + 'methods' => 'GET', + 'callback' => [__CLASS__, 'get_thread_api'], + 'permission_callback' => '__return_true', + ]); + register_rest_route('beep/v1', '/delete/(?P\d+)', [ + 'methods' => 'DELETE', + 'callback' => [__CLASS__, 'delete_beep'], + 'permission_callback' => ['Beep_Likes', 'can_interact'], + ]); + register_rest_route('beep/v1', '/delete-reply/(?P\d+)', [ + 'methods' => 'DELETE', + 'callback' => [__CLASS__, 'delete_reply'], + 'permission_callback' => ['Beep_Likes', 'can_interact'], + ]); + } + + public static function submit_beep($request) { + $content = sanitize_textarea_field($request->get_param('content')); + if (empty($content)) { + return new WP_Error('empty_content', 'Beep cannot be empty.', ['status' => 400]); + } + if (mb_strlen($content) > BEEP_CHAR_LIMIT) { + return new WP_Error('content_too_long', 'Beep exceeds ' . BEEP_CHAR_LIMIT . ' character limit.', ['status' => 400]); + } + + // Handle quote post ID + $quote_id = $request->get_param('quote_id'); + $gif_url = $request->get_param('gif_url'); + + $post_id = wp_insert_post([ + 'post_type' => 'beep', + 'post_content' => $content, + 'post_status' => 'publish', + 'post_author' => get_current_user_id(), + ]); + if (is_wp_error($post_id)) { return $post_id; } + + // Attach quote if provided + if ($quote_id && is_numeric($quote_id)) { + Beep_Quotes::create_quote($post_id, (int) $quote_id); + } + + // Attach GIF URL if provided + if ($gif_url && filter_var($gif_url, FILTER_VALIDATE_URL)) { + update_post_meta($post_id, 'beep_gif_url', esc_url_raw($gif_url)); + } + + return rest_ensure_response(['id' => $post_id, 'url' => get_permalink($post_id)]); + } + + public static function submit_reply($request) { + $post_id = (int) $request['id']; + $parent = get_post($post_id); + if (!$parent || $parent->post_type !== 'beep') { + return new WP_Error('invalid_post', 'Invalid beep post.', ['status' => 400]); + } + $content = sanitize_textarea_field($request->get_param('content')); + if (empty($content)) { + return new WP_Error('empty_content', 'Reply cannot be empty.', ['status' => 400]); + } + $comment_id = wp_insert_comment([ + 'comment_post_ID' => $post_id, + 'comment_content' => $content, + 'comment_author' => get_current_user_id() ? null : 'Anonymous', + 'comment_type' => 'comment', + 'user_id' => get_current_user_id(), + ]); + if (!$comment_id) { + return new WP_Error('insert_failed', 'Could not save reply.', ['status' => 500]); + } + return rest_ensure_response(['id' => $comment_id]); + } + + public static function delete_beep($request) { + $post_id = (int) $request['id']; + $post = get_post($post_id); + if (!$post || $post->post_type !== 'beep') { + return new WP_Error('invalid_post', 'Invalid beep.', ['status' => 400]); + } + if (!current_user_can('delete_post', $post_id)) { + return new WP_Error('forbidden', 'You do not have permission to delete this beep.', ['status' => 403]); + } + // Clean up media + Beep_Media::delete_media($post_id); + $deleted = wp_delete_post($post_id, true); + if (!$deleted) { + return new WP_Error('delete_failed', 'Could not delete.', ['status' => 500]); + } + return rest_ensure_response(['deleted' => true]); + } + + public static function delete_reply($request) { + $comment_id = (int) $request['id']; + $comment = get_comment($comment_id); + if (!$comment) { + return new WP_Error('invalid_reply', 'Reply not found.', ['status' => 404]); + } + // Only allow the reply author or a moderator to delete + $can_delete = ( + (int) $comment->user_id === get_current_user_id() || + current_user_can('moderate_comments') + ); + if (!$can_delete) { + return new WP_Error('forbidden', 'You do not have permission to delete this reply.', ['status' => 403]); + } + $deleted = wp_delete_comment($comment_id, true); + if (!$deleted) { + return new WP_Error('delete_failed', 'Could not delete.', ['status' => 500]); + } + return rest_ensure_response(['deleted' => true]); + } + + public static function get_replies($post_id) { + return get_comments([ + 'post_id' => $post_id, + 'status' => 'approve', + 'order' => 'ASC', + 'type' => 'comment', + ]); + } + + public static function reply_count($post_id) { + return (int) get_comments_number($post_id); + } + + public static function get_thread($post_id, $depth = 0, $max_depth = 10) { + if ($depth > $max_depth) return []; + $post = get_post($post_id); + if (!$post || $post->post_type !== 'beep') return []; + $thread = [self::format_beep_reply($post, $depth)]; + $replies = get_comments([ + 'post_id' => $post_id, 'status' => 'approve', 'order' => 'ASC', + 'type' => 'comment', 'number' => 100, + ]); + foreach ($replies as $reply) { + if ($depth < $max_depth) { + $thread = array_merge($thread, self::get_comment_thread($reply->comment_ID, $depth + 1, $max_depth)); + } + } + return $thread; + } + + private static function get_comment_thread($comment_id, $depth, $max_depth) { + if ($depth > $max_depth) return []; + $comment = get_comment($comment_id); + if (!$comment) return []; + $result = [self::format_comment_reply($comment, $depth)]; + $children = get_comments([ + 'parent' => $comment_id, 'status' => 'approve', 'order' => 'ASC', + 'type' => 'comment', 'number' => 100, + ]); + foreach ($children as $child) { + $result = array_merge($result, self::get_comment_thread($child->comment_ID, $depth + 1, $max_depth)); + } + return $result; + } + + public static function get_thread_api($request) { + $post_id = (int) $request['post_id']; + $thread = self::get_thread($post_id); + return rest_ensure_response(['thread' => $thread]); + } + + public static function format_beep_reply($post, $depth = 0) { + $author_id = $post->post_author; + $author = get_userdata($author_id); + $avatar = get_user_meta($author_id, 'beep_avatar', true); + if (!$avatar) { + $avatar = 'https://www.gravatar.com/avatar/' . md5(strtolower($author->user_email ?? '')) . '?s=48&d=mp'; + } + return [ + 'id' => $post->ID, + 'parent_id' => $post->post_parent ?: null, + 'is_post' => true, + 'depth' => $depth, + 'content' => $post->post_content, + 'time_ago' => self::time_ago(strtotime($post->post_date)), + 'like_count' => Beep_Likes::get_count($post->ID, 'post'), + 'reply_count' => self::reply_count($post->ID), + 'author' => [ + 'id' => $author_id, + 'name' => $author->display_name ?? 'Unknown', + 'username' => $author->user_login ?? '', + 'avatar' => $avatar, + ], + ]; + } + + public static function format_comment_reply($comment, $depth = 0) { + $author_id = (int) $comment->user_id; + $author = $author_id ? get_userdata($author_id) : null; + if ($author_id) { + $avatar = get_user_meta($author_id, 'beep_avatar', true); + if (!$avatar) { + $avatar = 'https://www.gravatar.com/avatar/' . md5(strtolower($author->user_email ?? '')) . '?s=48&d=mp'; + } + $name = $author->display_name ?? 'Unknown'; + $username = $author->user_login ?? ''; + } else { + $avatar = 'https://www.gravatar.com/avatar/?s=48&d=mp'; + $name = esc_html($comment->comment_author); + $username = ''; + } + return [ + 'id' => $comment->comment_ID, + 'parent_id' => (int) $comment->comment_parent ?: null, + 'is_post' => false, + 'depth' => $depth, + 'content' => $comment->comment_content, + 'time_ago' => self::time_ago(strtotime($comment->comment_date)), + 'like_count' => Beep_Likes::get_count($comment->comment_ID, 'reply'), + 'reply_count' => 0, + 'author' => [ + 'id' => $author_id, + 'name' => $name, + 'username' => $username, + 'avatar' => $avatar, + ], + ]; + } + + public static function time_ago($timestamp) { + $diff = time() - $timestamp; + if ($diff < 0) $diff = 0; + if ($diff < 60) return $diff . 's'; + if ($diff < 3600) return floor($diff / 60) . 'm'; + if ($diff < 86400) return floor($diff / 3600) . 'h'; + if ($diff < 604800) return floor($diff / 86400) . 'd'; + return gmdate('M j', $timestamp); + } +} diff --git a/includes/class-settings.php b/includes/class-settings.php new file mode 100755 index 0000000..5370994 --- /dev/null +++ b/includes/class-settings.php @@ -0,0 +1,1118 @@ + '#FFFFFF', // Twitter Light: white + 'bg_card' => '#F5F8FA', // Twitter Light: card background + 'bg_card_hover' => '#EBEEF0', // Twitter Light: hover state + 'bg_input' => '#F5F8FA', // Twitter Light: input fields + + // Colors - Text + 'text_primary' => '#0F1419', // Twitter Light: main text (near black) + 'text_secondary' => '#536471', // Twitter Light: muted text + 'text_link' => '#1D9BF0', // Twitter Light: links (Twitter blue) + + // Colors - Borders & Dividers + 'border_color' => '#EFF3F4', // Twitter Light: borders and dividers + 'border_hover' => '#CBD0D4', // Twitter Light: hover border + + // Colors - Interactive + 'color_like' => '#F91880', // Like heart (Twitter pink) + 'color_repost' => '#00BA7C', // Repost green + 'color_reply' => '#1D9BF0', // Reply blue + 'color_quote' => '#1D9BF0', // Quote blue + 'color_poll' => '#1D9BF0', // Poll blue + + // Colors - Buttons + 'btn_primary_bg' => '#1D9BF0', // Primary button background + 'btn_primary_text' => '#FFFFFF', // Primary button text + 'btn_secondary_bg' => '#2F3336', // Secondary button background + 'btn_secondary_text' => '#E7E9EA', // Secondary button text + + // Colors - Avatar Ring + 'avatar_ring' => '#2F3336', // Avatar ring color + + // Branding + 'logo_url' => '', // Custom logo URL + 'icon_url' => '', // Custom icon URL (favicon) + 'site_name' => 'Beep', // Or 'X' or 'Twitter' + 'tagline' => 'Messages from', + + // Layout + 'feed_max_width' => '600px', // Max width of feed + 'feed_padding' => '16px', // Feed padding + 'post_padding' => '14px 20px', // Post padding + 'avatar_size' => '48px', // Avatar dimensions + 'gap_between_posts' => '12px', // Gap between posts + + // Border Radius + 'radius_card' => '16px', // Card border radius + 'radius_avatar' => '9999px', // Avatar circle + 'radius_button' => '9999px', // Buttons/pills + 'radius_input' => '4px', // Input fields + + // Typography + 'font_family' => '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + 'font_size_base' => '15px', // Base font size + 'font_size_small' => '13px', // Small text (timestamps) + 'font_size_large' => '20px', // Large text (names) + + // Features (on/off) + 'enable_likes' => '1', + 'enable_reposts' => '1', + 'enable_quotes' => '1', + 'enable_polls' => '1', + 'enable_follows' => '1', + 'enable_media' => '1', + 'enable_replies' => '1', + + // Behavior + 'character_limit' => '280', + 'posts_per_page' => '20', + 'auto_refresh' => '0', // Auto refresh interval (0 = off) + 'public_posting' => '0', // Allow public posting without login + + // Content + 'show_avatar' => '1', + 'show_stats' => '1', // Show reply/like counts + 'show_timestamps' => '1', // Show relative time + 'show_gravatar' => '1', // Show Gravatar + 'show_public_notice' => '1', // Show "sign in to post" notice + + // Animation + 'animation_speed' => '150', // Transition speed in ms + ]; + + /** + * Initialize settings + */ + public static function init() { + add_action('admin_init', [__CLASS__, 'register_settings']); + add_action('admin_menu', [__CLASS__, 'add_settings_page']); + add_filter('plugin_action_links_' . plugin_basename(BEEP_FILE), [__CLASS__, 'add_settings_link']); + add_action('wp_head', [__CLASS__, 'inject_css_variables'], 100); + } + + /** + * Get all settings with defaults + */ + public static function get_settings() { + $saved = get_option(self::OPTION_KEY, []); + return wp_parse_args($saved, self::$defaults); + } + + /** + * Get a single setting + */ + public static function get($key, $default = null) { + $settings = self::get_settings(); + return isset($settings[$key]) ? $settings[$key] : ($default ?? (self::$defaults[$key] ?? null)); + } + + /** + * Register settings with WordPress + */ + public static function register_settings() { + register_setting( + self::OPTION_KEY, + self::OPTION_KEY, + [ + 'type' => 'array', + 'sanitize_callback' => [__CLASS__, 'sanitize_settings'], + 'default' => [], + ] + ); + + // === Section: Colors - Backgrounds === + add_settings_section( + 'beep_colors_bg', + 'Colors - Backgrounds', + [__CLASS__, 'section_colors_bg_description'], + 'beep-settings' + ); + + add_settings_field( + 'bg_main', + 'Main Background', + [__CLASS__, 'field_color_picker'], + 'beep-settings', + 'beep_colors_bg', + ['key' => 'bg_main', 'label' => 'Feed/Page background'] + ); + + add_settings_field( + 'bg_card', + 'Card Background', + [__CLASS__, 'field_color_picker'], + 'beep-settings', + 'beep_colors_bg', + ['key' => 'bg_card', 'label' => 'Post cards & containers'] + ); + + add_settings_field( + 'bg_card_hover', + 'Card Hover Background', + [__CLASS__, 'field_color_picker'], + 'beep-settings', + 'beep_colors_bg', + ['key' => 'bg_card_hover', 'label' => 'Hover state for cards'] + ); + + add_settings_field( + 'bg_input', + 'Input Background', + [__CLASS__, 'field_color_picker'], + 'beep-settings', + 'beep_colors_bg', + ['key' => 'bg_input', 'label' => 'Textarea/input fields'] + ); + + // === Section: Colors - Text === + add_settings_section( + 'beep_colors_text', + 'Colors - Text', + [__CLASS__, 'section_colors_text_description'], + 'beep-settings' + ); + + add_settings_field( + 'text_primary', + 'Primary Text', + [__CLASS__, 'field_color_picker'], + 'beep-settings', + 'beep_colors_text', + ['key' => 'text_primary', 'label' => 'Main text color'] + ); + + add_settings_field( + 'text_secondary', + 'Secondary Text', + [__CLASS__, 'field_color_picker'], + 'beep-settings', + 'beep_colors_text', + ['key' => 'text_secondary', 'label' => 'Muted/timestamp text'] + ); + + add_settings_field( + 'text_link', + 'Link Color', + [__CLASS__, 'field_color_picker'], + 'beep-settings', + 'beep_colors_text', + ['key' => 'text_link', 'label' => 'Links & buttons'] + ); + + // === Section: Colors - Interactive === + add_settings_section( + 'beep_colors_interactive', + 'Colors - Interactive Elements', + [__CLASS__, 'section_colors_interactive_description'], + 'beep-settings' + ); + + add_settings_field( + 'color_like', + 'Like Heart Color', + [__CLASS__, 'field_color_picker'], + 'beep-settings', + 'beep_colors_interactive', + ['key' => 'color_like', 'label' => 'Heart/like button'] + ); + + add_settings_field( + 'color_repost', + 'Repost Color', + [__CLASS__, 'field_color_picker'], + 'beep-settings', + 'beep_colors_interactive', + ['key' => 'color_repost', 'label' => 'Repost button'] + ); + + add_settings_field( + 'color_reply', + 'Reply Color', + [__CLASS__, 'field_color_picker'], + 'beep-settings', + 'beep_colors_interactive', + ['key' => 'color_reply', 'label' => 'Reply button'] + ); + + add_settings_field( + 'border_color', + 'Border Color', + [__CLASS__, 'field_color_picker'], + 'beep-settings', + 'beep_colors_interactive', + ['key' => 'border_color', 'label' => 'Dividers & borders'] + ); + + // === Section: Layout === + add_settings_section( + 'beep_layout', + 'Layout & Spacing', + [__CLASS__, 'section_layout_description'], + 'beep-settings' + ); + + add_settings_field( + 'feed_max_width', + 'Feed Max Width', + [__CLASS__, 'field_text'], + 'beep-settings', + 'beep_layout', + ['key' => 'feed_max_width', 'label' => 'e.g. 600px or 100%'] + ); + + add_settings_field( + 'feed_padding', + 'Feed Padding', + [__CLASS__, 'field_text'], + 'beep-settings', + 'beep_layout', + ['key' => 'feed_padding', 'label' => 'e.g. 16px'] + ); + + add_settings_field( + 'post_padding', + 'Post Padding', + [__CLASS__, 'field_text'], + 'beep-settings', + 'beep_layout', + ['key' => 'post_padding', 'label' => 'e.g. 14px 20px'] + ); + + add_settings_field( + 'avatar_size', + 'Avatar Size', + [__CLASS__, 'field_text'], + 'beep-settings', + 'beep_layout', + ['key' => 'avatar_size', 'label' => 'e.g. 48px'] + ); + + // === Section: Border Radius === + add_settings_section( + 'beep_radius', + 'Border Radius', + [__CLASS__, 'section_radius_description'], + 'beep-settings' + ); + + add_settings_field( + 'radius_card', + 'Card Radius', + [__CLASS__, 'field_text'], + 'beep-settings', + 'beep_radius', + ['key' => 'radius_card', 'label' => 'e.g. 16px for cards, 0 for sharp edges'] + ); + + add_settings_field( + 'radius_avatar', + 'Avatar Radius', + [__CLASS__, 'field_text'], + 'beep-settings', + 'beep_radius', + ['key' => 'radius_avatar', 'label' => 'e.g. 9999px for circle'] + ); + + // === Section: Typography === + add_settings_section( + 'beep_typography', + 'Typography', + [__CLASS__, 'section_typography_description'], + 'beep-settings' + ); + + add_settings_field( + 'font_family', + 'Font Family', + [__CLASS__, 'field_text'], + 'beep-settings', + 'beep_typography', + ['key' => 'font_family', 'label' => 'CSS font-family value'] + ); + + add_settings_field( + 'font_size_base', + 'Base Font Size', + [__CLASS__, 'field_text'], + 'beep-settings', + 'beep_typography', + ['key' => 'font_size_base', 'label' => 'e.g. 15px'] + ); + + // === Section: Branding & Media === + add_settings_section( + 'beep_branding', + 'Branding & Media', + [__CLASS__, 'section_branding_description'], + 'beep-settings' + ); + + add_settings_field( + 'logo_url', + 'Logo Image', + [__CLASS__, 'field_media_upload'], + 'beep-settings', + 'beep_branding', + ['key' => 'logo_url', 'label' => 'Upload a logo image. Leave empty to use text.'] + ); + + add_settings_field( + 'icon_url', + 'Icon Image', + [__CLASS__, 'field_media_upload'], + 'beep-settings', + 'beep_branding', + ['key' => 'icon_url', 'label' => 'Upload an icon/favicon image.'] + ); + + add_settings_field( + 'site_name', + 'Site Name', + [__CLASS__, 'field_text'], + 'beep-settings', + 'beep_branding', + ['key' => 'site_name', 'label' => 'e.g. Beep, X, or Twitter'] + ); + + add_settings_field( + 'tagline', + 'Tagline', + [__CLASS__, 'field_text'], + 'beep-settings', + 'beep_branding', + ['key' => 'tagline', 'label' => 'Text above logo, e.g. "Messages from"'] + ); + + // === Section: Features === + add_settings_section( + 'beep_features', + 'Features', + [__CLASS__, 'section_features_description'], + 'beep-settings' + ); + + add_settings_field( + 'enable_likes', + 'Enable Likes', + [__CLASS__, 'field_checkbox'], + 'beep-settings', + 'beep_features', + ['key' => 'enable_likes', 'label' => 'Show like button on posts'] + ); + + add_settings_field( + 'enable_reposts', + 'Enable Reposts', + [__CLASS__, 'field_checkbox'], + 'beep-settings', + 'beep_features', + ['key' => 'enable_reposts', 'label' => 'Show repost button'] + ); + + add_settings_field( + 'enable_quotes', + 'Enable Quotes', + [__CLASS__, 'field_checkbox'], + 'beep-settings', + 'beep_features', + ['key' => 'enable_quotes', 'label' => 'Show quote button'] + ); + + add_settings_field( + 'enable_replies', + 'Enable Replies', + [__CLASS__, 'field_checkbox'], + 'beep-settings', + 'beep_features', + ['key' => 'enable_replies', 'label' => 'Allow replying to posts'] + ); + + add_settings_field( + 'enable_media', + 'Enable Media', + [__CLASS__, 'field_checkbox'], + 'beep-settings', + 'beep_features', + ['key' => 'enable_media', 'label' => 'Allow image attachments'] + ); + + add_settings_field( + 'enable_polls', + 'Enable Polls', + [__CLASS__, 'field_checkbox'], + 'beep-settings', + 'beep_features', + ['key' => 'enable_polls', 'label' => 'Allow poll creation'] + ); + + // === Section: Behavior === + add_settings_section( + 'beep_behavior', + 'Behavior', + [__CLASS__, 'section_behavior_description'], + 'beep-settings' + ); + + add_settings_field( + 'character_limit', + 'Character Limit', + [__CLASS__, 'field_text'], + 'beep-settings', + 'beep_behavior', + ['key' => 'character_limit', 'label' => 'Max characters per post (default 280)'] + ); + + add_settings_field( + 'posts_per_page', + 'Posts Per Page', + [__CLASS__, 'field_text'], + 'beep-settings', + 'beep_behavior', + ['key' => 'posts_per_page', 'label' => 'Number of posts to load'] + ); + + add_settings_field( + 'public_posting', + 'Allow Public Posting', + [__CLASS__, 'field_checkbox'], + 'beep-settings', + 'beep_behavior', + ['key' => 'public_posting', 'label' => 'Allow posting without logging in'] + ); + + // === Section: Content Display === + add_settings_section( + 'beep_content', + 'Content Display', + [__CLASS__, 'section_content_description'], + 'beep-settings' + ); + + add_settings_field( + 'show_avatar', + 'Show Avatars', + [__CLASS__, 'field_checkbox'], + 'beep-settings', + 'beep_content', + ['key' => 'show_avatar', 'label' => 'Display user profile pictures'] + ); + + add_settings_field( + 'show_stats', + 'Show Statistics', + [__CLASS__, 'field_checkbox'], + 'beep-settings', + 'beep_content', + ['key' => 'show_stats', 'label' => 'Display reply/like counts'] + ); + + add_settings_field( + 'show_timestamps', + 'Show Timestamps', + [__CLASS__, 'field_checkbox'], + 'beep-settings', + 'beep_content', + ['key' => 'show_timestamps', 'label' => 'Display relative time'] + ); + + add_settings_field( + 'show_public_notice', + 'Show Public Notice', + [__CLASS__, 'field_checkbox'], + 'beep-settings', + 'beep_content', + ['key' => 'show_public_notice', 'label' => 'Show "Sign in to post" notice'] + ); + } + + /** + * Add settings page to admin menu + */ + public static function add_settings_page() { + add_options_page( + 'Beep Settings', + 'Beep', + 'manage_options', + 'beep-settings', + [__CLASS__, 'render_settings_page'] + ); + } + + /** + * Add settings link on Plugins page + */ + public static function add_settings_link($links) { + $settings_link = '' . __('Settings') . ''; + array_unshift($links, $settings_link); + return $links; + } + + /** + * Sanitize settings on save + */ + public static function sanitize_settings($input) { + $sanitized = []; + + foreach (self::$defaults as $key => $default) { + if (is_bool($default)) { + $sanitized[$key] = !empty($input[$key]) ? '1' : '0'; + } elseif (is_int($default)) { + $sanitized[$key] = isset($input[$key]) ? intval($input[$key]) : $default; + } else { + $sanitized[$key] = isset($input[$key]) ? sanitize_text_field($input[$key]) : $default; + } + } + + return $sanitized; + } + + /** + * Render the settings page + */ + public static function render_settings_page() { + if (!current_user_can('manage_options')) { + return; + } + ?> +
+

Beep Settings

+ +
+

Customize the appearance and behavior of your Beep feed. Changes take effect immediately.

+

+ Quick Presets: + + + +

+
+ +
+ +
+ +
+ + +
+ + + + + + */ + public static function inject_css_variables() { + $settings = self::get_settings(); + ?> + + Background colors for the feed and cards.

'; + } + + public static function section_colors_text_description() { + echo '

Text colors throughout the feed.

'; + } + + public static function section_colors_interactive_description() { + echo '

Colors for interactive elements like buttons and icons.

'; + } + + public static function section_layout_description() { + echo '

Spacing and layout dimensions.

'; + } + + public static function section_radius_description() { + echo '

Border radius for rounded corners. Use 0px for sharp edges (Twitter style), 16px for rounded cards.

'; + } + + public static function section_typography_description() { + echo '

Font settings.

'; + } + + public static function section_branding_description() { + echo '

Logo, icon, and site name settings.

'; + } + + public static function section_features_description() { + echo '

Toggle features on or off.

'; + } + + public static function section_behavior_description() { + echo '

How the feed behaves.

'; + } + + public static function section_content_description() { + echo '

What to show/hide in the feed.

'; + } + + // === Field Renderers === + public static function field_color_picker($args) { + $key = $args['key']; + $label = $args['label']; + $value = self::get($key); + ?> + + +

+ + +

+ + + + + +
+ Recommended for :
+ + +
Transparent PNG/SVG — the header has a blurred backdrop + +
Will be displayed at 32×32px in posts, 24×24px in replies + +
+ +
+ +
+ + +
+ + +
+ + + +
+ + No image chosen yet — click "Choose Image" or paste a URL above +
+ +
+ + + + +
+ +

+ 'GET', + 'callback' => [__CLASS__, 'fetch_feed'], + 'permission_callback' => '__return_true', + 'args' => [ + 'sort' => ['default' => 'latest'], + 'limit' => ['default' => 50, 'validate_callback' => function ($v) { return is_numeric($v); }], + 'offset' => ['default' => 0, 'validate_callback' => function ($v) { return is_numeric($v); }], + ], + ]); + } + + /** + * REST endpoint: re-render the feed list with the given sort. + */ + public static function fetch_feed($request) { + $sort = $request->get_param('sort') === 'top' ? 'top' : 'latest'; + $limit = max(1, (int) $request->get_param('limit')); + $offset = max(0, (int) $request->get_param('offset')); + $posts = ($sort === 'top') ? self::get_top_beeps($limit + 1, $offset) : self::get_latest_beeps($limit + 1, $offset); + + $has_more = count($posts) > $limit; + if ($has_more) { + $posts = array_slice($posts, 0, $limit); + } + + $html = ''; + foreach ($posts as $p) { + $html .= beep_render_post($p); + } + if ($html === '' && $offset === 0) { + $empty_bird = beep_bird_svg( ['size' => 64, 'fill' => '#4a90c4'] ); + $html = '
' . $empty_bird . '

No beeps yet.

Be the first to share something!
'; + } + return rest_ensure_response(['html' => $html, 'sort' => $sort, 'has_more' => $has_more, 'offset' => $offset + count($posts)]); + } + + public static function get_latest_beeps($limit, $offset = 0) { + global $wpdb; + return $wpdb->get_results($wpdb->prepare( + "SELECT * FROM {$wpdb->posts} + WHERE post_type = %s AND post_status = 'publish' + ORDER BY post_date DESC + LIMIT %d OFFSET %d", + Beep_CPT::POST_TYPE, $limit, $offset + )); + } + + public static function get_top_beeps($limit, $offset = 0) { + global $wpdb; + $likes = Beep_Likes::table_name(); + return $wpdb->get_results($wpdb->prepare( + "SELECT p.*, COALESCE(l.cnt, 0) AS like_count + FROM {$wpdb->posts} p + LEFT JOIN ( + SELECT post_id, COUNT(*) AS cnt + FROM $likes + WHERE object_type = 'post' + GROUP BY post_id + ) l ON l.post_id = p.ID + WHERE p.post_type = %s AND p.post_status = 'publish' + ORDER BY COALESCE(l.cnt, 0) DESC, p.post_date DESC + LIMIT %d OFFSET %d", + Beep_CPT::POST_TYPE, $limit, $offset + )); + } + + public static function render_feed($atts) { + $atts = shortcode_atts([ + 'limit' => 50, + 'header' => 'Beep!', + 'show_wordmark' => true, + 'sort' => 'latest', + ], $atts, 'beep_feed'); + + $limit = max(1, (int) $atts['limit']); + $sort = ($atts['sort'] === 'top') ? 'top' : 'latest'; + $posts = ($sort === 'top') ? self::get_top_beeps($limit) : self::get_latest_beeps($limit); + $has_posts = !empty($posts); + + ob_start(); + ?> +
+
+
+
Messages from Beep!
+
+ + +
+ + +
+ + + + +
+ 64, 'fill' => '#4a90c4'] ); + echo '
' . $empty_bird . '

No beeps yet.

Be the first to share something!
'; + } + ?> +
+
+
+ \d+)', [ + 'methods' => 'POST', + 'callback' => [__CLASS__, 'upload_media'], + 'permission_callback' => function () { + return is_user_logged_in() && !beep_user_is_banned(get_current_user_id()); + }, + ]); + register_rest_route('beep/v1', '/media/(?P\d+)/(?P\d+)', [ + 'methods' => 'DELETE', + 'callback' => [__CLASS__, 'delete_media'], + 'permission_callback' => function () { + return is_user_logged_in() && !beep_user_is_banned(get_current_user_id()); + }, + ]); + register_rest_route('beep/v1', '/voice/(?P\d+)', [ + 'methods' => 'POST', + 'callback' => [__CLASS__, 'upload_voice'], + 'permission_callback' => function () { + return is_user_logged_in() && !beep_user_is_banned(get_current_user_id()); + }, + ]); + } + + public static function upload_media($request) { + $post_id = (int) $request->get_param('post_id'); + $post = get_post($post_id); + + if (!$post || $post->post_type !== Beep_CPT::POST_TYPE) { + return new WP_Error('not_found', 'Beep not found.', 404); + } + if (!current_user_can('edit_post', $post_id)) { + return new WP_Error('forbidden', 'Cannot edit this beep.', 403); + } + if (empty($_FILES['file'])) { + return new WP_Error('no_file', 'No file uploaded.'); + } + + $result = Beep_Media::handle_upload($post_id, $_FILES['file']); + if (is_wp_error($result)) { + return $result; + } + return rest_ensure_response($result); + } + + public static function delete_media($request) { + $post_id = (int) $request->get_param('post_id'); + $attachment_id = (int) $request->get_param('attachment_id'); + $post = get_post($post_id); + + if (!$post || $post->post_type !== Beep_CPT::POST_TYPE) { + return new WP_Error('not_found', 'Beep not found.', 404); + } + if (!current_user_can('edit_post', $post_id)) { + return new WP_Error('forbidden', 'Cannot edit this beep.', 403); + } + + Beep_Media::remove_image($post_id, $attachment_id); + return rest_ensure_response(['ok' => true]); + } + public static function upload_voice($request) { + $post_id = (int) $request->get_param('post_id'); + $post = get_post($post_id); + + if (!$post || $post->post_type !== Beep_CPT::POST_TYPE) { + return new WP_Error('not_found', 'Beep not found.', 404); + } + if (!current_user_can('edit_post', $post_id)) { + return new WP_Error('forbidden', 'Cannot edit this beep.', 403); + } + if (empty($_FILES['file'])) { + return new WP_Error('no_file', 'No file uploaded.'); + } + + $result = Beep_Media::handle_voice_upload($post_id, $_FILES['file']); + if (is_wp_error($result)) { + return $result; + } + return rest_ensure_response($result); + } + + + public static function register_thread_routes() { + register_rest_route('beep/v1', '/thread/(?P\d+)', [ + 'methods' => 'GET', + 'callback' => [__CLASS__, 'get_thread'], + 'permission_callback' => '__return_true', + ]); + } + + public static function get_thread($request) { + $post_id = (int) $request->get_param('post_id'); + if (get_post_type($post_id) !== Beep_CPT::POST_TYPE) { + return new WP_Error('not_found', 'Beep not found.', 404); + } + $depth = (int) $request->get_param('depth') ?: 10; + $thread = Beep_Replies::get_thread($post_id, 0, $depth); + return rest_ensure_response($thread); + } + + public static function register_quote_routes() { + register_rest_route('beep/v1', '/quote/(?P\d+)', [ + 'methods' => 'GET', + 'callback' => [__CLASS__, 'get_quoted_post'], + 'permission_callback' => '__return_true', + ]); + register_rest_route('beep/v1', '/lookup', [ + 'methods' => 'GET', + 'callback' => [__CLASS__, 'lookup_beep'], + 'permission_callback' => '__return_true', + ]); + } + + public static function get_quoted_post($request) { + $post_id = (int) $request->get_param('post_id'); + $quoted = Beep_Quotes::get_quoted_post($post_id); + if (!$quoted) { + return new WP_Error('not_found', 'No quoted post found.', 404); + } + return rest_ensure_response($quoted); + } + + public static function lookup_beep($request) { + $query = sanitize_text_field($request->get_param('q')); + if (!$query) { + return new WP_Error('missing_query', 'Query parameter q is required.', 400); + } + if (is_numeric($query)) { + $post = get_post((int) $query); + if ($post && $post->post_type === Beep_CPT::POST_TYPE) { + return rest_ensure_response(Beep_Quotes::get_quoted_post($post->ID)); + } + } + if (preg_match('/\[beep:(\d+)\]/', $query, $m)) { + $post = get_post((int) $m[1]); + if ($post && $post->post_type === Beep_CPT::POST_TYPE) { + return rest_ensure_response(Beep_Quotes::get_quoted_post($post->ID)); + } + } + return new WP_Error('not_found', 'Beep not found.', 404); + } +} diff --git a/includes/class-shortcode.php.bak b/includes/class-shortcode.php.bak new file mode 100755 index 0000000..8fd52c4 --- /dev/null +++ b/includes/class-shortcode.php.bak @@ -0,0 +1,292 @@ + 'GET', + 'callback' => [__CLASS__, 'fetch_feed'], + 'permission_callback' => '__return_true', + 'args' => [ + 'sort' => ['default' => 'latest'], + 'limit' => ['default' => 50, 'validate_callback' => function ($v) { return is_numeric($v); }], + 'offset' => ['default' => 0, 'validate_callback' => function ($v) { return is_numeric($v); }], + ], + ]); + } + + /** + * REST endpoint: re-render the feed list with the given sort. + */ + public static function fetch_feed($request) { + $sort = $request->get_param('sort') === 'top' ? 'top' : 'latest'; + $limit = max(1, (int) $request->get_param('limit')); + $offset = max(0, (int) $request->get_param('offset')); + $posts = ($sort === 'top') ? self::get_top_beeps($limit + 1, $offset) : self::get_latest_beeps($limit + 1, $offset); + + $has_more = count($posts) > $limit; + if ($has_more) { + $posts = array_slice($posts, 0, $limit); + } + + $html = ''; + foreach ($posts as $p) { + $html .= beep_render_post($p); + } + if ($html === '' && $offset === 0) { + $empty_bird = beep_bird_svg( ['size' => 64, 'fill' => '#4a90c4'] ); + $html = '
' . $empty_bird . '

No beeps yet.

Be the first to share something!
'; + } + return rest_ensure_response(['html' => $html, 'sort' => $sort, 'has_more' => $has_more, 'offset' => $offset + count($posts)]); + } + + public static function get_latest_beeps($limit, $offset = 0) { + global $wpdb; + return $wpdb->get_results($wpdb->prepare( + "SELECT * FROM {$wpdb->posts} + WHERE post_type = %s AND post_status = 'publish' + ORDER BY post_date DESC + LIMIT %d OFFSET %d", + Beep_CPT::POST_TYPE, $limit, $offset + )); + } + + public static function get_top_beeps($limit, $offset = 0) { + global $wpdb; + $likes = Beep_Likes::table_name(); + return $wpdb->get_results($wpdb->prepare( + "SELECT p.*, COALESCE(l.cnt, 0) AS like_count + FROM {$wpdb->posts} p + LEFT JOIN ( + SELECT post_id, COUNT(*) AS cnt + FROM $likes + WHERE object_type = 'post' + GROUP BY post_id + ) l ON l.post_id = p.ID + WHERE p.post_type = %s AND p.post_status = 'publish' + ORDER BY COALESCE(l.cnt, 0) DESC, p.post_date DESC + LIMIT %d OFFSET %d", + Beep_CPT::POST_TYPE, $limit, $offset + )); + } + + public static function render_feed($atts) { + $atts = shortcode_atts([ + 'limit' => 50, + 'header' => 'Beep!', + 'show_wordmark' => true, + 'sort' => 'latest', + ], $atts, 'beep_feed'); + + $limit = max(1, (int) $atts['limit']); + $sort = ($atts['sort'] === 'top') ? 'top' : 'latest'; + $posts = ($sort === 'top') ? self::get_top_beeps($limit) : self::get_latest_beeps($limit); + $has_posts = !empty($posts); + + ob_start(); + ?> +
+
+
+
Messages from Beep!
+
+ + +
+ + +
+ + + + + +
+ Your account has been suspended from beeping. +
+ + + + +
+ 64, 'fill' => '#4a90c4'] ); + echo '
' . $empty_bird . '

No beeps yet.

Be the first to share something!
'; + } + ?> +
+
+
+ \d+)', [ + 'methods' => 'POST', + 'callback' => [__CLASS__, 'upload_media'], + 'permission_callback' => function () { + return is_user_logged_in() && !beep_user_is_banned(get_current_user_id()); + }, + ]); + register_rest_route('beep/v1', '/media/(?P\d+)/(?P\d+)', [ + 'methods' => 'DELETE', + 'callback' => [__CLASS__, 'delete_media'], + 'permission_callback' => function () { + return is_user_logged_in() && !beep_user_is_banned(get_current_user_id()); + }, + ]); + register_rest_route('beep/v1', '/voice/(?P\d+)', [ + 'methods' => 'POST', + 'callback' => [__CLASS__, 'upload_voice'], + 'permission_callback' => function () { + return is_user_logged_in() && !beep_user_is_banned(get_current_user_id()); + }, + ]); + } + + public static function upload_media($request) { + $post_id = (int) $request->get_param('post_id'); + $post = get_post($post_id); + + if (!$post || $post->post_type !== Beep_CPT::POST_TYPE) { + return new WP_Error('not_found', 'Beep not found.', 404); + } + if (!current_user_can('edit_post', $post_id)) { + return new WP_Error('forbidden', 'Cannot edit this beep.', 403); + } + if (empty($_FILES['file'])) { + return new WP_Error('no_file', 'No file uploaded.'); + } + + $result = Beep_Media::handle_upload($post_id, $_FILES['file']); + if (is_wp_error($result)) { + return $result; + } + return rest_ensure_response($result); + } + + public static function delete_media($request) { + $post_id = (int) $request->get_param('post_id'); + $attachment_id = (int) $request->get_param('attachment_id'); + $post = get_post($post_id); + + if (!$post || $post->post_type !== Beep_CPT::POST_TYPE) { + return new WP_Error('not_found', 'Beep not found.', 404); + } + if (!current_user_can('edit_post', $post_id)) { + return new WP_Error('forbidden', 'Cannot edit this beep.', 403); + } + + Beep_Media::remove_image($post_id, $attachment_id); + return rest_ensure_response(['ok' => true]); + } + public static function upload_voice($request) { + $post_id = (int) $request->get_param('post_id'); + $post = get_post($post_id); + + if (!$post || $post->post_type !== Beep_CPT::POST_TYPE) { + return new WP_Error('not_found', 'Beep not found.', 404); + } + if (!current_user_can('edit_post', $post_id)) { + return new WP_Error('forbidden', 'Cannot edit this beep.', 403); + } + if (empty($_FILES['file'])) { + return new WP_Error('no_file', 'No file uploaded.'); + } + + $result = Beep_Media::handle_voice_upload($post_id, $_FILES['file']); + if (is_wp_error($result)) { + return $result; + } + return rest_ensure_response($result); + } + + + public static function register_thread_routes() { + register_rest_route('beep/v1', '/thread/(?P\d+)', [ + 'methods' => 'GET', + 'callback' => [__CLASS__, 'get_thread'], + 'permission_callback' => '__return_true', + ]); + } + + public static function get_thread($request) { + $post_id = (int) $request->get_param('post_id'); + if (get_post_type($post_id) !== Beep_CPT::POST_TYPE) { + return new WP_Error('not_found', 'Beep not found.', 404); + } + $depth = (int) $request->get_param('depth') ?: 10; + $thread = Beep_Replies::get_thread($post_id, 0, $depth); + return rest_ensure_response($thread); + } + + public static function register_quote_routes() { + register_rest_route('beep/v1', '/quote/(?P\d+)', [ + 'methods' => 'GET', + 'callback' => [__CLASS__, 'get_quoted_post'], + 'permission_callback' => '__return_true', + ]); + register_rest_route('beep/v1', '/lookup', [ + 'methods' => 'GET', + 'callback' => [__CLASS__, 'lookup_beep'], + 'permission_callback' => '__return_true', + ]); + } + + public static function get_quoted_post($request) { + $post_id = (int) $request->get_param('post_id'); + $quoted = Beep_Quotes::get_quoted_post($post_id); + if (!$quoted) { + return new WP_Error('not_found', 'No quoted post found.', 404); + } + return rest_ensure_response($quoted); + } + + public static function lookup_beep($request) { + $query = sanitize_text_field($request->get_param('q')); + if (!$query) { + return new WP_Error('missing_query', 'Query parameter q is required.', 400); + } + if (is_numeric($query)) { + $post = get_post((int) $query); + if ($post && $post->post_type === Beep_CPT::POST_TYPE) { + return rest_ensure_response(Beep_Quotes::get_quoted_post($post->ID)); + } + } + if (preg_match('/\[beep:(\d+)\]/', $query, $m)) { + $post = get_post((int) $m[1]); + if ($post && $post->post_type === Beep_CPT::POST_TYPE) { + return rest_ensure_response(Beep_Quotes::get_quoted_post($post->ID)); + } + } + return new WP_Error('not_found', 'Beep not found.', 404); + } +} diff --git a/includes/helpers.php b/includes/helpers.php new file mode 100755 index 0000000..82241e2 --- /dev/null +++ b/includes/helpers.php @@ -0,0 +1,589 @@ +' + . '' + . '' + . '' + . ''; +} + +/** + * Wordmark (bird + "Beep!") shown at the top of the feed. + * Uses the real Sami-designed bird PNG with CSS color filter + * to match brand colours. + */ +function beep_logo_svg() { + // Allow site-owners to swap in a custom logo via settings. + $custom = Beep_Settings::get( 'logo_url' ); + if ( $custom ) { + return ''; + } + + // Real bird PNG as — CSS filter turns black silhouette → brand blue. + $filter = 'brightness(0) saturate(100%) invert(30%) sepia(100%) saturate(10000%) hue-rotate(190deg)'; + return ''; +} + +/** + * Is this user banned from posting/liking/replying? + */ +function beep_user_is_banned($user_id) { + if (!$user_id) { + return false; + } + return get_user_meta($user_id, 'beep_banned', true) === '1'; +} + +/** + * Render the composer textarea (for logged-in, non-banned users). + */ +function beep_render_composer() { + $user = wp_get_current_user(); + $beep_avatar = get_user_meta($post->post_author, 'beep_avatar', true); + if ($beep_avatar) { + $avatar_url = $beep_avatar; + $filter = ''; + } elseif ($author && !empty($author->user_email)) { + $avatar_url = 'https://www.gravatar.com/avatar/' . md5(strtolower($author->user_email)) . '?s=96&d=mp'; + $filter = ''; + } else { + $avatar_url = BEEP_URL . 'assets/beep-bird.png'; + $filter = 'brightness(0) saturate(100%) invert(22%) sepia(100%) saturate(10000%) hue-rotate(190deg)'; + } + $avatar = ''; + + ob_start(); + ?> +
+ +
+ + +
+ + +
+ post_author); + $beep_avatar = get_user_meta($post->post_author, 'beep_avatar', true); + if ($beep_avatar) { + $avatar_url = $beep_avatar; + $filter = ''; + } elseif ($author && !empty($author->user_email)) { + $avatar_url = 'https://www.gravatar.com/avatar/' . md5(strtolower($author->user_email)) . '?s=96&d=mp'; + $filter = ''; + } else { + $avatar_url = BEEP_URL . 'assets/beep-bird.png'; + $filter = 'brightness(0) saturate(100%) invert(22%) sepia(100%) saturate(10000%) hue-rotate(190deg)'; + } + $avatar = ''; + $name = $author ? esc_html($author->display_name) : 'Unknown'; + $handle = $author ? '@' . esc_html($author->user_login) : ''; + $ts = strtotime($post->post_date_gmt . ' UTC'); + $time = beep_relative_time($ts); + $time_iso = gmdate('c', $ts); + $time_full = mysql2date('F j, Y g:i a', $post->post_date); + + $content = beep_format_content($post->post_content); + $like_count = Beep_Likes::get_count($post->ID, 'post'); + $reply_count = Beep_Replies::reply_count($post->ID); + $user_liked = Beep_Likes::user_has_liked($post->ID, 'post'); + $can_delete = current_user_can('delete_post', $post->ID); + $can_interact = is_user_logged_in() && !beep_user_is_banned(get_current_user_id()); + $beep_images = Beep_Media::get_images($post->ID); + $beep_video = Beep_Media::get_video_url($post->ID); + $beep_voice = Beep_Media::get_voice_url($post->ID); + + ob_start(); + ?> +
+ +
+
+ 18, 'fill' => '#0064cd', 'class' => 'beep-post-badge'] ); ?> + + + + + · + + +
+
+ +
+ +
+ + +
+ +
+ + +
+ ID); ?> +
+ + ID); + ?> + +
+ ID); ?> +
+ + ID, 'beep_gif_url', true); + ?> + +
+
+ GIF +
+
+ + ID); + if ($poll) : ?> +
+
+
+ + +
+
+ + + % + +
+ + + + +
+
+ votes + + + Final results + + · Ends + + +
+
+ +
+ + + + + + +
+ + + +
+ + + + Beep! +
+ + + ID, 0); ?> + + +
+ ID); + echo beep_render_replies_tree($all, 0, 0, $post->ID, $can_interact); + ?> +
+ Beep! +
+
+ + + comment_parent !== (int) $parent_id) { + continue; + } + $out .= beep_render_reply($c, $comments, $depth, $post_id, $can_interact); + } + return $out; +} + +/** + * Render a single reply, plus its nested children. + */ +function beep_render_reply($comment, $all_comments = [], $depth = 0, $post_id = 0, $can_interact = false) { + if (!$comment) { + return ''; + } + + $author_id = (int) $comment->user_id; + $avatar_url = BEEP_URL . 'assets/beep-bird.png'; + $filter = 'brightness(0) saturate(100%) invert(22%) sepia(100%) saturate(10000%) hue-rotate(190deg)'; + $avatar = ''; + + $author = $author_id ? get_userdata($author_id) : null; + $name = $author ? esc_html($author->display_name) : esc_html($comment->comment_author); + $handle = $author ? '@' . esc_html($author->user_login) : ''; + + $ts = strtotime($comment->comment_date_gmt . ' UTC'); + $time = beep_relative_time($ts); + $time_iso = gmdate('c', $ts); + + $content = beep_format_content($comment->comment_content); + $can_delete = current_user_can('moderate_comments'); + $like_count = Beep_Likes::get_count($comment->comment_ID, 'reply'); + $user_liked = Beep_Likes::user_has_liked($comment->comment_ID, 'reply'); + + $depth_class = 'beep-depth-' . min((int) $depth, 2); + + ob_start(); + ?> +
+ +
+
+ 16, 'fill' => '#0064cd', 'class' => 'beep-post-badge beep-post-badge-sm'] ); ?> + + + + + · + + + 16, 'fill' => '#0064cd', 'class' => 'beep-post-badge beep-post-badge-sm'] ); ?> +
+
+ + +
+ + +
+ comment_ID); ?> + + +
+ +
+ + + comment_ID, $depth + 1, $post_id, $can_interact); + if ($children) { + echo '
' . $children . '
'; + } + ?> +
+
+ #' . esc_html($m[2]) . ''; + }, $text); + // @mentions + $text = preg_replace_callback('/(^|\s)@([a-zA-Z0-9_]+)/', function ($m) { + $user = get_user_by('login', $m[2]); + if ($user) { + return $m[1] . '@' . esc_html($m[2]) . ''; + } + return $m[1] . '@' . esc_html($m[2]) . ''; + }, $text); + // URLs + $text = preg_replace_callback('#(https?://[^\s<]+)#i', function ($m) { + $url = esc_url($m[1]); + $display = parse_url($url, PHP_URL_HOST); + if (!$display) { + $display = $url; + } + return '' . esc_html($display) . ''; + }, $text); + // Line breaks + $text = nl2br($text); + return $text; +} + +function beep_relative_time($timestamp) { + $now = time(); + $diff = $now - $timestamp; + if ($diff < 0) { + $diff = 0; + } + if ($diff < 60) { + return $diff . 's'; + } + if ($diff < 3600) { + return floor($diff / 60) . 'm'; + } + if ($diff < 86400) { + return floor($diff / 3600) . 'h'; + } + if ($diff < 604800) { + return floor($diff / 86400) . 'd'; + } + return gmdate('M j', $timestamp); +} + +function beep_icon($name) { + $icons = [ + 'reply' => '', + 'heart' => '', + 'heart_filled' => '', + 'share' => '', + 'beep_bird' => '', + ]; + return isset($icons[$name]) ? $icons[$name] : ''; +} + + +/** + * Render the thread/conversation modal (sits outside individual posts). + */ +function beep_render_thread_modal() { + return ''; +} + + +/** + * Render the GIF picker modal. + */ +function beep_render_gif_modal() { + return ''; +} diff --git a/readme.txt b/readme.txt new file mode 100755 index 0000000..f73f93c --- /dev/null +++ b/readme.txt @@ -0,0 +1,81 @@ +=== Beep === +Contributors: sami +Tags: microblog, twitter, tweets, social, comments, polls, elementor +Requires at least: 6.0 +Tested up to: 6.9 +Requires PHP: 7.4 +Stable tag: 1.6.0 +License: GPLv2 or later + +A Twitter-style microblog for WordPress. Posts, likes, replies, polls, quotes, GIFs, and Gravatars. Use [beep_feed] shortcode or Elementor widget. + +== Description == + +Beep adds a Twitter-style microblog to your WordPress site. Drop the +[beep_feed] shortcode on any page or use the Elementor widget. Likes, +threaded replies, polls, quotes, GIFs, and Latest/Top sort tabs are all built in. + +Features: +* Twitter-style feed with [beep_feed] shortcode +* Elementor drag-and-drop widget +* Polls (create and vote) +* Quote posts +* GIF picker (Giphy integration) +* Likes on beeps and replies +* Nested threaded replies +* Gravatar avatars +* Admin settings page (Settings > Beep) +* Customizable colors and branding via CSS variables +* Bird-themed branding with custom icons + +== Installation == + +1. Upload beep.zip via Plugins -> Add New -> Upload Plugin. Activate. +2. Add [beep_feed] to a page, or use the Elementor widget. +3. For multi-user signup, enable "Anyone can register" in Settings -> General. + +== Shortcode == + +[beep_feed limit="50" header="Messages from" sort="latest"] + +== Changelog == + += 1.6.0 = +* Elementor drag-and-drop widget for feed placement. +* Admin settings page (Settings > Beep) for Giphy API key and branding. +* Polls: create polls in beeps, vote on polls. +* Quote posts: quote any beep. +* GIF picker powered by Giphy. +* Bird-themed redesign with custom bird icons and badges. +* CSS variables for customizable colors. +* Elementor widget registration. +* Settings stored in wp_options. + += 1.5.1 = +* Twitter-style UI redesign with bird assets. +* Beep bird badge on posts and replies. +* CSS scope isolation improvements. + += 1.5.0 = +* Polls feature (Beep_Polls class, voting UI). +* Bird wordmark logo in header. + += 1.4.0 = +* Like replies, not just beeps. +* Nested replies (reply to replies, indented up to two levels). +* Latest / Top sort tabs at the top of the feed. +* Bird badge in the top-right of every beep and reply. +* Polished tabs, focus states, action button styling. + += 1.3.0 = +* Feed header reads "Messages from" + Beep wordmark. +* Avatar/textarea fixes baked in. + += 1.2.0 = +* Feed header shows Beep wordmark instead of small icon + Home text. + += 1.1.0 = +* New hand-drawn bird logo. + += 1.0.0 = +* Initial release.