diff --git a/assets/beep.css b/assets/beep.css
index 108db19..d596fca 100755
--- a/assets/beep.css
+++ b/assets/beep.css
@@ -1,7 +1,6 @@
/* ============================================================
- Beep — Twitter-style microblog for WordPress
- All rules are scoped under .beep-feed so they don't leak
- into the host theme.
+ Beep — Twitter/X Dark Mode
+ Post and feed styling to match Twitter/X perfectly
============================================================ */
.beep-feed,
@@ -10,106 +9,99 @@
}
.beep-feed {
- --beep-bg: #ffffff;
- --beep-text: #0F1419;
- --beep-text-secondary: #536471;
- --beep-border: #EFF3F4;
- --beep-hover: #F7F9F9;
+ /* 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-page-bg: #E6ECF0;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
max-width: 600px;
- margin: 24px auto;
+ margin: 0 auto;
background: var(--beep-bg);
- border: 1px solid var(--beep-border);
- border-radius: 16px;
- overflow: hidden;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
- "Helvetica Neue", Arial, sans-serif;
+ 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: 100vh;
}
-/* ---------- Header ---------- */
+/* Header */
.beep-feed-header {
- padding: 14px 20px;
+ padding: 16px 20px;
border-bottom: 1px solid var(--beep-border);
- font-size: 20px;
- font-weight: 800;
- background: rgba(255, 255, 255, 0.85);
- backdrop-filter: blur(12px);
display: flex;
align-items: center;
- gap: 10px;
+ justify-content: space-between;
position: sticky;
top: 0;
z-index: 10;
+ background: rgba(21, 32, 43, 0.85);
+ backdrop-filter: blur(12px);
}
-.beep-feed-header-text {
+.beep-feed-header-title {
font-size: 20px;
font-weight: 800;
color: var(--beep-text);
- white-space: nowrap;
+ letter-spacing: -0.5px;
}
-.beep-feed-header .beep-logo {
- height: 36px;
- width: auto;
- max-width: 100%;
- flex-shrink: 0;
- object-fit: contain;
- display: block;
-}
-
-/* ---------- Notices ---------- */
-.beep-notice {
- padding: 24px 20px;
+/* Nav tabs */
+.beep-top-nav {
+ display: flex;
border-bottom: 1px solid var(--beep-border);
+}
+
+.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-notice p {
- margin: 0 0 12px;
- font-size: 16px;
+.beep-nav-tab:hover {
+ background: var(--beep-card-hover);
+ color: var(--beep-text);
}
-.beep-notice-banned {
- color: var(--beep-text-secondary);
- font-style: italic;
+.beep-nav-tab.is-active {
+ color: var(--beep-text);
}
-/* ---------- Avatars ---------- */
-.beep-feed .beep-avatar {
- width: 48px !important;
- height: 48px !important;
- min-width: 48px !important;
- max-width: 48px !important;
- border-radius: 50% !important;
- flex-shrink: 0;
- background: #ccd6dd;
- object-fit: cover !important;
- aspect-ratio: 1 / 1 !important;
- display: block !important;
+.beep-nav-tab.is-active::after {
+ content: '';
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ height: 4px;
+ background: var(--beep-accent);
+ border-radius: 9999px;
}
-.beep-feed .beep-avatar-sm {
- width: 36px !important;
- height: 36px !important;
- min-width: 36px !important;
- max-width: 36px !important;
-}
-
-/* ---------- Compose ---------- */
+/* Compose area */
.beep-compose {
display: flex;
gap: 12px;
- padding: 16px;
+ padding: 16px 20px;
border-bottom: 1px solid var(--beep-border);
}
@@ -123,102 +115,167 @@
border: 0 !important;
outline: 0 !important;
resize: none !important;
- background: #ffffff !important;
+ background: transparent !important;
font-family: inherit !important;
font-size: 18px !important;
- line-height: 1.4 !important;
+ line-height: 1.5 !important;
color: var(--beep-text) !important;
padding: 8px 0 !important;
- min-height: 32px;
+ min-height: 52px;
overflow: hidden;
box-shadow: none !important;
+ caret-color: var(--beep-accent);
}
.beep-feed .beep-input::placeholder {
- color: var(--beep-text-secondary) !important;
- opacity: 1 !important;
+ color: var(--beep-muted) !important;
}
-.beep-compose-footer,
-.beep-reply-footer {
+.beep-compose-footer {
display: flex;
- justify-content: flex-end;
+ justify-content: space-between;
align-items: center;
- gap: 12px;
+ margin-top: 12px;
+ padding-top: 12px;
border-top: 1px solid var(--beep-border);
- padding-top: 10px;
- margin-top: 4px;
}
-/* ---------- Char counter ---------- */
-.beep-char-counter {
- font-size: 13px;
- color: var(--beep-text-secondary);
- font-variant-numeric: tabular-nums;
+.beep-compose-tools {
+ display: flex;
+ gap: 4px;
}
-.beep-counter-warn { color: #FFD400; }
-.beep-counter-bad { color: #F4212E; font-weight: 700; }
+.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;
+}
-/* ---------- Buttons ---------- */
+.beep-compose-tool:hover {
+ background: rgba(29, 155, 240, 0.15);
+}
+
+/* Buttons */
.beep-button {
- display: inline-block;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
background: var(--beep-accent);
color: #fff;
border: 0;
border-radius: 9999px;
- padding: 8px 18px;
+ padding: 8px 20px;
font-weight: 700;
font-size: 15px;
cursor: pointer;
- text-decoration: none;
font-family: inherit;
- transition: background-color 0.15s ease, opacity 0.15s ease;
+ transition: background-color 0.15s ease, transform 0.1s ease;
}
-.beep-button:hover { background: var(--beep-accent-hover); color: #fff; }
+.beep-button:hover { background: var(--beep-accent-hover); color: #fff; }
+.beep-button:active { transform: scale(0.96); }
.beep-button:disabled { opacity: 0.5; cursor: default; }
-.beep-button:disabled:hover { background: var(--beep-accent); }
-.beep-button-secondary {
+/* Follow button */
+.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-follow-btn:hover { opacity: 0.85; }
+
+.beep-following-btn {
background: transparent;
- color: var(--beep-accent);
- border: 1px solid var(--beep-accent);
- margin-left: 8px;
-}
-.beep-button-secondary:hover {
- background: rgba(29, 155, 240, 0.1);
- color: var(--beep-accent);
+ 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;
}
-/* ---------- Post ---------- */
-.beep-list {
- background: var(--beep-bg);
+.beep-following-btn:hover {
+ border-color: #F4212E;
+ color: #F4212E;
+ background: rgba(244, 33, 46, 0.1);
}
+/* Char counter */
+.beep-char-counter { font-size: 13px; color: var(--beep-muted); font-variant-numeric: tabular-nums; }
+.beep-counter-warn { color: #FFD400; }
+.beep-counter-bad { color: #F4212E; font-weight: 700; }
+
+/* Avatars */
+.beep-feed .beep-avatar {
+ width: 48px !important;
+ height: 48px !important;
+ min-width: 48px !important;
+ max-width: 48px !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-feed .beep-avatar:hover { filter: brightness(0.9); }
+
+.beep-feed .beep-avatar-sm {
+ width: 40px !important;
+ height: 40px !important;
+ min-width: 40px !important;
+ max-width: 40px !important;
+}
+
+/* Post list */
+.beep-list { background: var(--beep-bg); }
+
.beep-empty {
- padding: 40px 20px;
+ padding: 60px 20px;
text-align: center;
- color: var(--beep-text-secondary);
+ color: var(--beep-muted);
}
+/* Individual post */
.beep-post {
display: flex;
gap: 12px;
- padding: 12px 16px;
+ padding: 14px 20px;
border-bottom: 1px solid var(--beep-border);
+ background: var(--beep-bg);
transition: background-color 0.15s ease;
+ cursor: pointer;
}
-.beep-post:hover { background: var(--beep-hover); }
+.beep-post:hover { background: var(--beep-card-hover); }
.beep-post:last-child { border-bottom: 0; }
-.beep-post-body {
- flex: 1;
- min-width: 0;
-}
+.beep-post-body { flex: 1; min-width: 0; }
-/* ---------- Meta line ---------- */
+/* Meta line */
.beep-meta {
display: flex;
align-items: center;
@@ -232,84 +289,64 @@
.beep-name {
font-weight: 700;
color: var(--beep-text);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
+.beep-name:hover { text-decoration: underline; cursor: pointer; }
+
.beep-handle,
.beep-dot,
-.beep-time {
- color: var(--beep-text-secondary);
- font-weight: 400;
-}
+.beep-time { color: var(--beep-muted); font-weight: 400; white-space: nowrap; }
.beep-time { text-decoration: none; }
+.beep-time:hover { text-decoration: underline; cursor: pointer; }
.beep-spacer { flex: 1; }
-.beep-menu-trigger {
- background: transparent;
- border: 0;
- color: var(--beep-text-secondary);
- cursor: pointer;
- font-size: 18px;
- line-height: 1;
- width: 24px;
- height: 24px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 0;
- transition: background-color 0.15s ease, color 0.15s ease;
+/* Verified badge */
+.beep-verified {
+ width: 18px;
+ height: 18px;
+ fill: var(--beep-accent);
+ flex-shrink: 0;
+ margin-left: 2px;
+ vertical-align: text-bottom;
}
-.beep-menu-trigger:hover {
- background: rgba(244, 33, 46, 0.1);
- color: #F4212E;
-}
-
-/* ---------- Text ---------- */
+/* Post text */
.beep-text {
font-size: 15px;
- line-height: 1.4;
+ line-height: 1.5;
color: var(--beep-text);
word-wrap: break-word;
overflow-wrap: anywhere;
- margin: 2px 0 12px;
+ margin: 0;
+ white-space: pre-wrap;
}
-.beep-tag,
-.beep-mention,
-.beep-link {
- color: var(--beep-accent);
- text-decoration: none;
-}
+.beep-tag, .beep-mention { color: var(--beep-text); text-decoration: none; cursor: pointer; }
+.beep-tag:hover, .beep-mention:hover { text-decoration: underline; }
-.beep-tag:hover,
-.beep-mention:hover,
-.beep-link:hover {
- text-decoration: underline;
-}
+.beep-link { color: var(--beep-accent); text-decoration: none; }
+.beep-link:hover { text-decoration: underline; }
-.beep-mention-stale {
- color: var(--beep-accent);
- opacity: 0.85;
-}
-
-/* ---------- Action row ---------- */
+/* Action bar */
.beep-actions {
display: flex;
justify-content: space-between;
max-width: 425px;
- margin-top: 4px;
+ margin-top: 12px;
}
.beep-action {
display: flex;
align-items: center;
- gap: 6px;
+ gap: 4px;
background: transparent;
border: 0;
- color: var(--beep-text-secondary);
+ color: var(--beep-muted);
font-size: 13px;
font-family: inherit;
cursor: pointer;
@@ -319,144 +356,522 @@
transition: color 0.15s ease, background-color 0.15s ease;
}
-.beep-action svg {
+.beep-action svg,
+.beep-action .beep-icon-svg {
width: 18px;
height: 18px;
fill: currentColor;
display: block;
+ flex-shrink: 0;
}
-.beep-action .beep-count {
+.beep-count {
font-variant-numeric: tabular-nums;
+ min-width: 20px;
}
-.beep-action-reply:hover { color: var(--beep-accent); background: rgba(29,155,240,0.10); }
-.beep-action-like:hover { color: var(--beep-like); background: rgba(249, 24,128,0.10); }
-.beep-action-share:hover { color: var(--beep-accent); background: rgba(29,155,240,0.10); }
+/* Reply - blue */
+.beep-action-reply:hover {
+ color: var(--beep-accent);
+ background: rgba(29, 155, 240, 0.1);
+}
-/* Like state */
-.beep-action-like .beep-icon-filled { display: none; }
-.beep-action-like .beep-icon-outline { display: inline-flex; }
+/* Retweet - green */
+.beep-action-retweet:hover {
+ color: var(--beep-repost);
+ background: rgba(0, 186, 124, 0.1);
+}
-.beep-action-like.is-liked {
+.beep-action.is-reposted { color: var(--beep-repost); }
+
+/* Like - pink */
+.beep-action-like:hover {
color: var(--beep-like);
+ background: rgba(249, 24, 128, 0.1);
}
-.beep-action-like.is-liked .beep-icon-outline { display: none; }
-.beep-action-like.is-liked .beep-icon-filled { display: inline-flex; }
-/* Heart "pop" animation when newly liked */
+.beep-action.is-liked { color: var(--beep-like); }
+
+/* Bookmark - blue */
+.beep-action-bookmark:hover {
+ color: var(--beep-accent);
+ background: rgba(29, 155, 240, 0.1);
+}
+
+.beep-action.is-bookmarked { color: var(--beep-accent); }
+
+/* Share - blue */
+.beep-action-share:hover {
+ color: var(--beep-accent);
+ background: rgba(29, 155, 240, 0.1);
+}
+
+/* Heart pop animation */
.beep-action-like.beep-just-liked .beep-icon-filled {
animation: beep-heart-pop 0.55s ease;
}
@keyframes beep-heart-pop {
- 0% { transform: scale(0.5); }
- 35% { transform: scale(1.35); }
- 60% { transform: scale(0.85); }
+ 0% { transform: scale(0.85); }
+ 40% { transform: scale(1.35); }
+ 70% { transform: scale(0.9); }
100% { transform: scale(1); }
}
-/* ---------- Reply form ---------- */
-.beep-reply-form {
- margin-top: 8px;
- padding: 10px 0 4px;
- border-top: 1px solid var(--beep-border);
+/* Retweet spin animation */
+.beep-action-retweet.beep-just-retweeted svg {
+ animation: beep-retweet-spin 0.4s ease;
}
-.beep-reply-form .beep-input {
- font-size: 16px;
+@keyframes beep-retweet-spin {
+ 0% { transform: rotate(0deg) scale(1); }
+ 50% { transform: rotate(180deg) scale(1.2); }
+ 100% { transform: rotate(360deg) scale(1); }
}
-.beep-reply-form[hidden] { display: none; }
+/* Media */
+.beep-media-attachments {
+ margin-top: 12px;
+ border-radius: 16px;
+ overflow: hidden;
+ border: 1px solid var(--beep-border);
+}
-.beep-pending {
- padding: 12px;
- background: var(--beep-hover);
- color: var(--beep-text-secondary);
- border-radius: 8px;
+.beep-media-attachments img {
+ width: 100%;
+ height: auto;
+ display: block;
+ max-height: 400px;
+ object-fit: cover;
+}
+
+/* Image grid */
+.beep-media-grid {
+ display: grid;
+ gap: 2px;
+ border-radius: 16px;
+ overflow: hidden;
+ border: 1px solid var(--beep-border);
+}
+
+.beep-media-grid-2 { grid-template-columns: 1fr 1fr; }
+.beep-media-grid-3 .beep-media-grid-item:first-child { grid-column: span 2; }
+.beep-media-grid-4 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }
+
+.beep-media-grid-item {
+ overflow: hidden;
+ aspect-ratio: 16 / 9;
+}
+
+.beep-media-grid-item img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ max-height: none;
+}
+
+/* Polls */
+.beep-poll-container {
+ margin-top: 12px;
+ border: 1px solid var(--beep-border);
+ border-radius: 16px;
+ padding: 16px;
+ background: transparent;
+}
+
+.beep-poll-question {
+ font-size: 15px;
+ font-weight: 700;
+ color: var(--beep-text);
+ margin-bottom: 12px;
+}
+
+.beep-poll-options {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.beep-poll-option {
+ border: 1px solid var(--beep-border);
+ border-radius: 9999px;
+ padding: 10px 16px;
+ cursor: pointer;
font-size: 14px;
- text-align: center;
+ font-weight: 700;
+ color: var(--beep-text);
+ background: transparent;
+ transition: background-color 0.15s ease, border-color 0.15s ease;
+ position: relative;
+ overflow: hidden;
}
-/* ---------- Replies ---------- */
+.beep-poll-option:hover {
+ background: var(--beep-card-hover);
+ border-color: var(--beep-muted);
+}
+
+.beep-poll-option.is-voted { border-color: var(--beep-accent); }
+
+.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-poll-result-bar {
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ background: var(--beep-accent);
+ opacity: 0.3;
+ max-width: 100%;
+}
+
+.beep-poll-result.is-voted { border-color: var(--beep-accent); }
+.beep-poll-result-text { position: relative; z-index: 1; }
+.beep-poll-end { font-size: 13px; color: var(--beep-muted); margin-top: 8px; font-weight: 400; }
+
+/* Quotes */
+.beep-quoted-container {
+ margin-top: 12px;
+ border: 1px solid var(--beep-border);
+ border-radius: 16px;
+ padding: 12px 16px;
+ background: transparent;
+}
+
+.beep-quoted-post { text-decoration: none; color: inherit; }
+.beep-quoted-post:hover { text-decoration: none; }
+
+.beep-quoted-meta { font-size: 13px; color: var(--beep-muted); margin-bottom: 4px; }
+.beep-quoted-meta .beep-name { font-weight: 700; }
+
+.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-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-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-voice-play svg { width: 14px; height: 14px; fill: #fff; }
+
+.beep-voice-waveform {
+ flex: 1;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ gap: 2px;
+}
+
+.beep-voice-bar {
+ width: 3px;
+ height: 100%;
+ background: var(--beep-accent);
+ border-radius: 9999px;
+ opacity: 0.6;
+}
+
+.beep-voice-duration { font-size: 13px; color: var(--beep-muted); font-variant-numeric: tabular-nums; }
+
+/* Replies */
.beep-replies {
margin-top: 8px;
- padding-left: 24px;
+ padding-left: 0;
border-left: 2px solid var(--beep-border);
+ margin-left: 24px;
}
.beep-reply {
display: flex;
gap: 10px;
- padding: 10px 0;
+ padding: 10px 16px;
border-bottom: 1px solid var(--beep-border);
+ background: var(--beep-bg);
+ transition: background-color 0.15s ease;
}
.beep-reply:last-child { border-bottom: 0; }
+.beep-reply-body { flex: 1; min-width: 0; }
+.beep-reply .beep-text { font-size: 14px; margin-bottom: 0; }
+.beep-reply .beep-meta { font-size: 13px; margin-bottom: 0; }
+.beep-reply .beep-avatar { width: 36px !important; height: 36px !important; min-width: 36px !important; max-width: 36px !important; }
-.beep-reply-body {
- flex: 1;
- min-width: 0;
+/* Reply form */
+.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-reply .beep-text {
- font-size: 14px;
- margin-bottom: 0;
+.beep-reply-form[hidden] { display: none; }
+.beep-reply-form-body { flex: 1; min-width: 0; }
+
+.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);
}
-/* ---------- New-post entry animation ---------- */
-.beep-new {
- animation: beep-slide-in 0.4s ease;
+.beep-feed .beep-reply-input::placeholder { color: var(--beep-muted) !important; }
+
+.beep-reply-footer {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ margin-top: 8px;
}
+/* Animations */
+.beep-new { animation: beep-slide-in 0.3s ease; }
+
@keyframes beep-slide-in {
- from { opacity: 0; transform: translateY(-6px); }
+ from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
-/* ---------- Mobile ---------- */
-@media (max-width: 640px) {
- .beep-feed {
- margin: 0 auto;
- border-radius: 0;
- border-left: 0;
- border-right: 0;
- }
- .beep-actions { max-width: none; }
+/* Post badge */
+.beep-post-badge {
+ width: 18px;
+ height: 18px;
+ flex-shrink: 0;
+ opacity: 0.5;
+ margin-left: 2px;
}
-/* ---------- Tip popover (for "Copied" etc) ---------- */
+.beep-post-badge:hover { opacity: 0.8; cursor: pointer; }
+
+/* Loading */
+.beep-loading { opacity: 0.5; pointer-events: none; }
+
+/* Mobile */
+@media (max-width: 640px) {
+ .beep-feed { margin: 0; border-left: 0; border-right: 0; }
+ .beep-post { padding: 12px 16px; }
+ .beep-actions { max-width: none; }
+ .beep-compose { padding: 12px 16px; }
+ .beep-feed-header { padding: 12px 16px; }
+}
+
+/* Delete */
+.beep-delete-row { display: flex; justify-content: flex-end; margin-top: 6px; }
+
+.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-delete-link:hover { color: #F4212E; background: rgba(244, 33, 46, 0.1); }
+
+/* Notice */
+.beep-notice { padding: 14px 20px; border-bottom: 1px solid var(--beep-border); font-size: 14px; }
+.beep-notice p { margin: 0; color: var(--beep-muted); }
+.beep-notice-banned { color: #F4212E; font-style: italic; }
+
+/* Header post button */
+.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-post-btn-header:hover { background: var(--beep-accent-hover); }
+.beep-post-btn-header:active { transform: scale(0.96); }
+
+/* Back link */
+.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-back-link:hover { background: var(--beep-card-hover); color: var(--beep-text); text-decoration: none; }
+.beep-back-link svg { width: 20px; height: 20px; fill: currentColor; }
+
+/* Thread modal */
+.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-thread-modal[hidden] { display: none; }
+
+.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-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-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-thread-close:hover { background: var(--beep-card-hover); }
+.beep-thread-close svg { width: 20px; height: 20px; fill: currentColor; }
+.beep-thread-title { font-size: 18px; font-weight: 700; color: var(--beep-text); }
+
+/* Tooltip */
.beep-tip {
position: absolute;
margin-left: 8px;
- padding: 2px 8px;
+ padding: 4px 10px;
background: var(--beep-text);
- color: #fff;
+ 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; }
- 20% { opacity: 1; }
+ 0% { opacity: 0; transform: translateY(-4px); }
+ 20% { opacity: 1; transform: translateY(0); }
80% { opacity: 1; }
100% { opacity: 0; }
}
-/* ============================================================
- v1.4 — sort tabs, nested replies, Twitter polish
- ============================================================ */
-
-/* ---------- Sort tabs (Latest / Top) ---------- */
-.beep-feed .beep-tabs {
- display: flex;
- border-bottom: 1px solid var(--beep-border);
- background: var(--beep-bg);
+/* GIF */
+.beep-gif-embed {
+ margin-top: 12px;
+ border-radius: 12px;
+ overflow: hidden;
+ border: 1px solid var(--beep-border);
}
-.beep-feed .beep-tab {
+.beep-gif-embed img {
+ width: 100%;
+ height: auto;
+ max-height: 400px;
+ object-fit: cover;
+ display: block;
+ margin: 0;
+ border-radius: 12px;
+}
+
+/* Profile header */
+.beep-profile-header { padding: 16px 20px; border-bottom: 1px solid var(--beep-border); }
+.beep-profile-name { font-size: 20px; font-weight: 800; color: var(--beep-text); margin-bottom: 2px; }
+.beep-profile-meta { font-size: 14px; color: var(--beep-muted); }
+.beep-profile-stats { display: flex; gap: 20px; margin-top: 12px; }
+.beep-profile-stat { color: var(--beep-muted); font-size: 14px; cursor: pointer; }
+.beep-profile-stat:hover { text-decoration: underline; }
+.beep-profile-stat strong { color: var(--beep-text); font-weight: 700; }
+
+/* Tabs */
+.beep-tabs {
+ display: flex;
+ border-bottom: 1px solid var(--beep-border);
+ background: var(--beep-card-bg);
+}
+
+.beep-tab {
flex: 1;
background: transparent;
border: 0;
@@ -464,22 +879,15 @@
font-family: inherit;
font-size: 15px;
font-weight: 700;
- color: var(--beep-text-secondary);
+ color: var(--beep-muted);
cursor: pointer;
position: relative;
transition: background-color 0.15s ease, color 0.15s ease;
}
-.beep-feed .beep-tab:hover {
- background: var(--beep-hover);
- color: var(--beep-text);
-}
-
-.beep-feed .beep-tab.is-active {
- color: var(--beep-text);
-}
-
-.beep-feed .beep-tab.is-active::after {
+.beep-tab:hover { background: var(--beep-card-hover); color: var(--beep-text); }
+.beep-tab.is-active { color: var(--beep-text); }
+.beep-tab.is-active::after {
content: '';
position: absolute;
left: 50%;
@@ -491,1077 +899,145 @@
background: var(--beep-accent);
}
-/* ---------- Loading state on feed list ---------- */
-.beep-feed .beep-list.beep-loading {
- opacity: 0.5;
- pointer-events: none;
- transition: opacity 0.15s ease;
-}
-
-/* ---------- Action row variant for replies (smaller, no share) ---------- */
-.beep-feed .beep-actions-reply {
- max-width: 240px;
- gap: 24px;
- margin-top: 2px;
-}
-
-/* ---------- Nested replies: visual indent, capped at depth 2 ---------- */
-.beep-feed .beep-replies-nested {
- margin-top: 8px;
- margin-left: 12px;
- padding-left: 14px;
- border-left: 2px solid var(--beep-border);
-}
-
-/* Anything deeper than depth 2 stops indenting more */
-.beep-feed .beep-depth-2 .beep-replies-nested,
-.beep-feed .beep-depth-3 .beep-replies-nested,
-.beep-feed .beep-depth-4 .beep-replies-nested {
- margin-left: 0;
- padding-left: 8px;
-}
-
-/* ---------- Focus rings on text inputs ---------- */
+/* Focus */
.beep-feed .beep-input:focus,
-.beep-feed .beep-input:focus-visible {
+.beep-feed .beep-input:focus-visible,
+.beep-feed .beep-reply-input:focus,
+.beep-feed .beep-reply-input:focus-visible {
outline: 0 !important;
box-shadow: none !important;
}
-.beep-feed .beep-compose:focus-within,
-.beep-feed .beep-reply-form:focus-within {
- background: linear-gradient(to bottom, transparent, rgba(29, 155, 240, 0.04));
- transition: background-color 0.15s ease;
+.beep-feed .beep-compose:focus-within { background: rgba(29, 155, 240, 0.03); }
+
+/* Hidden */
+[hidden] { display: none !important; }
+
+
+/* Magic Login Form */
+.beep-magic-login {
+ padding: 24px;
+ display: flex;
+ justify-content: center;
}
-/* ---------- Buttons: refine to match Twitter's "Post" button ---------- */
-.beep-feed .beep-post-button,
-.beep-feed .beep-reply-submit {
- font-size: 14px;
- padding: 7px 18px;
- font-weight: 700;
- letter-spacing: -0.1px;
+.beep-magic-login-card {
+ background: #192734;
+ border-radius: 16px;
+ padding: 32px;
+ width: 100%;
+ max-width: 400px;
+ text-align: center;
}
-/* ---------- Tighter post hover ---------- */
-.beep-feed .beep-post {
- transition: background-color 0.15s ease;
+.beep-magic-login-header {
+ margin-bottom: 24px;
}
-/* ---------- Refine action icons size ---------- */
-.beep-feed .beep-action {
- padding: 4px 6px;
- font-size: 13px;
-}
-.beep-feed .beep-action svg {
- width: 17px;
- height: 17px;
+.beep-magic-login-header svg {
+ margin-bottom: 16px;
}
-/* ---------- Reply rows: tighten spacing ---------- */
-.beep-feed .beep-reply {
- padding: 10px 0 8px;
+.beep-magic-login-header h3 {
+ color: #E7E9EA;
+ font-size: 20px;
+ font-weight: 700;
+ margin: 0 0 8px 0;
}
-/* Make sure reply meta + actions feel cohesive */
-.beep-feed .beep-reply .beep-text {
- margin-bottom: 4px;
+.beep-magic-login-header p {
+ color: #71767B;
+ font-size: 14px;
+ margin: 0;
}
-/* ---------- Per-post Beep badge (top-right of each post/reply) ---------- */
-.beep-feed .beep-post-badge {
- width: 20px;
- height: 20px;
- flex-shrink: 0;
- display: block;
- opacity: 0.75;
- margin-left: 4px;
- transition: opacity 0.15s ease;
-}
-.beep-feed .beep-post-badge:hover {
- opacity: 1;
-}
-.beep-feed .beep-post-badge-sm {
- width: 16px;
- height: 16px;
-}
-
-/* ---------- Delete row (bottom of post/reply, admin only) ---------- */
-.beep-feed .beep-delete-row {
- display: flex;
- justify-content: flex-end;
- margin-top: 6px;
-}
-
-.beep-feed .beep-delete-link {
- background: transparent;
- border: 0;
- color: var(--beep-text-secondary);
- font-family: inherit;
- font-size: 22px;
- line-height: 1;
- font-weight: 400;
- cursor: pointer;
- width: 28px;
- height: 28px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 0;
- transition: color 0.15s ease, background-color 0.15s ease, transform 0.1s ease;
-}
-
-.beep-feed .beep-delete-link:hover {
- color: #F4212E;
- background: rgba(244, 33, 46, 0.10);
-}
-
-.beep-feed .beep-delete-link:active {
- color: #F4212E;
- background: rgba(244, 33, 46, 0.25);
- transform: scale(0.92);
-}
-
-/* Override earlier 20/16 badge sizes with bigger 32/24 */
-.beep-feed .beep-post-badge {
- width: 32px;
- height: 32px;
-}
-.beep-feed .beep-post-badge-sm {
- width: 24px;
- height: 24px;
-}
-
-/* ============================================================
- v1.4.1 — theme-busting overrides
- Many WordPress themes style ALL
';
}
public static function activate() {
Beep_CPT::register();
Beep_Likes::create_table();
Beep_Polls::create_tables();
+ Beep_Magic_Links::create_table();
flush_rewrite_rules();
}
@@ -91,15 +42,12 @@ class Beep_Plugin {
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,
]);
}
-}
+}
\ No newline at end of file
diff --git a/includes/class-cpt.php b/includes/class-cpt.php
index bc02a47..9bbece3 100755
--- a/includes/class-cpt.php
+++ b/includes/class-cpt.php
@@ -30,7 +30,7 @@ class Beep_CPT {
'public' => true,
'show_in_menu' => true,
'show_in_rest' => true,
- 'menu_icon' => 'data:image/png;base64,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',
+ 'menu_icon' => 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAD0AAABACAYAAACp3n/2AAAae0lEQVR42t17eZycRZ33t+o5++l7es7M5JpMrkmGJEwIhyEdI0cQCaB0DKByLMYX8ABZWN5V3qb1XV1PcGU9cFkFQSQtSEKEgKBpI3JOQq5JyDHJkMlc3dN393NX7R89AYIJ8UBwrc+n/6uqrm/97u+vHoJ3dxAAHACaZq68QPQ1flqgUjNnrgKXqQwcRKAcVNQ5pRXOygN65fC3cr3rf//W9X/tId49wLEYRV+YNtnsTtUTuA6iAkcvli2jepC64ivcKZSIFvZT8MWixztFlDSYRsEwq8Y3xnb911cAbo4fmf/vAB2LCUgm3eY51z6qhusvtM0iLGPkX5zK3h/m+noKb53umxudpaD9Jo8ndA0RvaiWDzw3tv2BKBB3azMS7O8cdJwCCdYwJXazr7H96w4zsiW9cGV+508ee/Ms/5SzT/X5O2KgKimUsr+p9t37q9DMy1d4PeG7qCc4US8c/EGm94FrXz967CEBozsJUmB/ziWQd8mOUVe33K9MnNgnK/WRbKbvhlLfQ9+ZPPkKtb//XqthzqUfU7T6mcyq3CjL9R4OwHVtmIXM8vT+nz4Z7op1ecTIJiIFgtXMwX/J9b3wH0C/DcA96m+i/09EKuGeSP3fBdAxAUi6gYkfWRlum/ZQtVTYk96xqRvRmIHGORzJlajvuuJXXm/zudwyGWcWIyA2lTyqXi2uHenMXYJk0q2f++HPe/zTv0UcBtet5JnNB7lt7XRtJ+VW2G+ywz/d9aeeiP7NMUc7CQCieTxnUFHlXCnfD/SWAQDJlS6iUZLZfu95Ru7gLUQQKAdcxjnhhHMuOgqSSRcAMjse+XbVzl9v2fnHXJeVIAozRJ8/pobr79KaA6+0nLx6c8PCq28OdH90Wk2Y/LgCFf/mmAGkAK5oAcpAiJO380dpWCrlAuDELfzE0EM3Kp5QC+c2bGbDtIYfBoDAvCuuD4rBGWa6f4RJ4e9axuEPir7AREmNdENgk6isyJIQWgAqLGDZ3c0AbkJspYDkm9X/3QNNUo1zONApu3DP4XbFqgaUpwHwmvPBePiJ0+F9ibQc+fCyQKNzHaHVet2q/KC89/HfAYBCpIup7P2AGmkHFURotBkuGFzXAncsg5ulAYuObHBK5u8KBwbWAyBIJtl7Y9PdqyX03G23zFp1sRKa8kilOvZqetuP5oLDHf9nfqzE5Y9jex9tdmZtkNSmpXq5VKTU+iVMvd+F9Puymdlv9j85BMD8U4/1t5N0NC4ilbDrulaeJWjNdzFw6G7umwBcrFxDgZVvVT0OxCmioDUHlwRiAJJJNzz/0nNVteVkHaaRrw6d4xxY/9Kx/6+XA0n23njvaFREKuWIbWcuamrofFzRGiOlcv896a36dVjeTLChzgVOGFoIwIGTzmiYIHe9KKpNk3OZ/s+Xdt93BzrjMhrA0NjLkezkQIK/E+npXwcYgDZ75QebFlxbnHzarby+6/LvHzuarRFqictxNAVAY+dHPz/1fV/kjfOu31TThLj41wpL/FtIWGj94Pl+X8vDmuJVSsXB58yKs27ivH++AYLDmW3DLBV0XR95tJJcOfrmjO2ovVK9HAAENXQGtwk3DffHtTlx+veTex8BHJn3/kjzqY97/CHVNMZyoFJYlnwQqAecApwDcFzYVi7PYN9dzI3+vNq/bsuR9Uedq7090OL/wB5O5cZcefc0c+8zfce8oPcE9HgxEZwV69Y8dY/L3khjYXj4IUdJf1cLdKxhZT0tu9Z9XFYKjlVuhyCcIUmeJbKnntrVfKlU6r+2uG/dA0f2qSUWhHvbL24MB1pHbIHvHunZ0QWk3HfCdt8B9Y5TJL/kovmDk/1a8y8ULdSYL772nfy+e28AgPLyz7TjuTEHSB7lrb3tZ89VHfvzXk/4Ki009R5nxkey1eQvnqgBr8VwTQ1QIhBQx9WBlDN+GXivQRPEegn6TpYanJb7JC00pVgeWje27d4bEI9TJABsSJhvdkw1ewWr9CV2VICryYwrBK1+0id8Hv83q+BPILmGIbpUQAoOt/Mni/4GWGbZeSddD/0r7bhWI7NZ3/cHWpYUKoND6bGeawFOkHi95iUACFIJ5/UfEgyIiojHaWbklc85xbG0KoRn1k+6qBsgvDPdSAGAM1nkEECIKP59gI7FBKRSTmj2iiWS3PBPplFF1Sh/FIe2DAIr3+xsjhNDUw42gqKwNe/YZpKqPoGr4UUAUKlotXMRkXMiACCDNUu6nbyXoAk6O7mveX6DpjTfI1APKtWhHxm7HtpUU+Ok++fsxam9g3MOJlqnA4AkBTgASJrfJoSASOoWAMDGd6Yq/Ms2ia2hSCSY5uu8WfE2dVT0ob78zg03Ix6n40X8iS8tGheRBgXAJUmoqQSBAwC2XSQAQK3yqQDAzaL5Tqr3X2ArMQHJndw77dw5qr/+U65ZQckc/b9AroDERhGAc0LAAK/Z9rikLaFV8FKuwfdyHgCmTAH6ASZ6QwCBY7r0vbXpWAxAgqlS8+2iJxiosMxWY9fjj6F7tTQeR48DNE5rc8AByA0dq37YNv+TpwDgjLqXuMwkemV4DAC86VrIEgTSxDkHEX3PAwAae/l7IOk4RXKl65963gxPIHCuZet8bDTzNQA6eu4+4rcIYkkKJN9Ylky6QIKjBwyA0tR59V3eSPs1ZunATuCc/aLH1247ZZ3Y+WcBoDcGBwmAU3Eud01uFQdrl5l8L9Q7CooUmOhv+axANL/rmG6jt/UGYd7H/EXdeb6yZ8sBgJSOwVhI4Vnnz5TEpnNUQbiUquGFZmXYKo09/6vgtHlXyUpIrOjpP2QPpwZisZiQTCRcr/fUJkp4u+OaZqFQ3T5+e+zdBk2QSjgNnVGfTJSVzDYZhyl4Q6FFDvMvClAbvq6lOZClO5hTLAg2Y4zIgqAECGXmNHi0maLsBTOrcMFY2cz9onho5/4J8xf/q+0apFrKfhcAkltKIgBXbotMkxSfVzdKW1HJ5sA5ASH83bXpaFwAACZP/rikhRoM09gzWtq3oFIa+Ckl1CLMtkWRqIqinenxT/qQ0tCxQgnWny9JwgcFX91M2zVf06uHn3UA27IrpLDzyX+uX3jlVxWtrk4vZgfKua1PAZwgOIkBIAINrCCCypnpbAN6bKxcSd99R5a63QU6ZZmLNzmiBFOmd9p7f/2K4qubxUVBts3Dq3L54S+7ouyabrF3LL9ntVHJbmOKB5Vq8YXRvnUXu/B4RDkgGfrYrc1T5s/wksgtplNF2T18JcZeLSG2kqKnxQXARUlezFwQ27Q3AQBGO8m7CzoWEwCC8PSpZ0lqZJpTHB0rbvneg41d1zwoqi2nVIujD6eHBg/WhaZeyx2LlMYGd/rhX+wNNHU5ennb2MgLN7a1n/e1gH/iycVs39OF7MEkbez8GahKq3rmbnPPhmdqhUYnBxLMU7eoTRC8J1lGPq/rxobapSfYuwu6s5MD4KIcuYlLKnJC6ftN8y/7ujfQtKpqjGwuV/q/0zKp+xdUohNds4pw3ZSYEm77hG7mD4xl93xjwoRFd6j+yWcVyvt/W0jvva1pcvfDohicUC31r8lvf+CziK0RkEwyRGvn0VpmXip7In7LKf/GGHzoUI1hwTsG+k9wZDEBiQQLzzp7riwHFjvFjOsnwmUef2t7pZLPjw7su7N5Ssf3ZFo3lTk2ZEmiLnF7iqW+/6gWy/PrGjruldU6OpQdWFux9n63uf2U+71S/bRK5fC29IHUjeDcAhnPqZeCIdWtKbL/kxwMlmk8U1PtnX+papNYLEZHRztJ6k39rhODjo4SpMBVte1DgihLBBaR1bZ208j8tmgObmxobb9DgTdSKR0+zBzPD00383twm/h9LVcHmpovZ4Qjlxu4n+Wrh1omzH9MlTRPMbt/s6MNL4N+qABye604icZFJBJOaMZlVylqcHqlmN9T3FN8EOAEKeL+uWhjsZiQTCbdZPKP6wBywjAFAHOXNbbIs3bLkhayXGvIqBZ+7CimWyc1XkcELcLsKgyzWiKC+CxlrFWW1C6i+qEXM4cKmfTTWmPI9amRaxiRUBobeKyw975LAVTGzYsdoYDUKadPrg8v6BGkUCQ7krmx1H/3neNU8l9WT69Y4e9Om0tUfzhaLuQe3/rCkxvj8Tilf0KY4g2k7TpZCYUqFfPZYn7sFo/ieV/Y234bc0gkmxu+p2SVcx7V5/co4eWyJ9Jl2WysnN17W9kY+054wsRzgoHGaxyrXMwauTsKe32XgZDKOMFXAxzrJUCnHKzr/L6ghSNVa+TlEl79AXCiAiZOozX2lbwupHiNXV105sWfWmgHdquBpvWKGriZg30SADZu3EjFt005G+dw34wP1StC4CrHMhi41RQKBO8TVB8xy8OvFErO8z6oTapX9HJI0CvZl0yDf8M0D08PhCJnNwQblnIwFMZGN9v69kurB17aM15hk9dVOpVwkASCC65Zq8qN5zl6Nm8oucvQnzKApcdnPmMxAcmEm3qjPUTi8ThJJBJs/qkXPqL46i52bR22a44QQusMUywdWSocV627Z4p4+lbH23bKDxSl7kzCbCZJUr1Dbafill4STA5/MLRCC9fNNnW7mh0o3F8tDP3C29h0fshfd52iNk4zS8XRklH6bH7Xj6+38oMZxGICemMEsUaK3p1A//tZS/tZk3ytSx8KBJrPM8uFfmOw7+LCnse2vR3rGY1Gxf7HH3fnzTtjfvvcRbcHI/Vb04OvFVOpFO9aeN71/kj9TbZl5qu5wYs4FQRFC5xSLuXuyg73bZ0yZcoxJU3AOUCIXT991aUeGr7ccXTXtQvUdkRHUhUalIKnspBGbH1sR7Ew+KDheiaEW7WQTKd9n6hBj2WlOTMOfG1058CdQGoY8TjF+iEJoy0cSLi1woGgpWvVpYpU9zXirZtYLuTzhUPD11q5Dc+/nR2POyin5YyLzpFl8QHFE66XKvYggC9NW7hwohbwf8V1gMGB0Qv8Z3W+4nl17GHTqLiqyzbVmqRLmXjMVgohNDLnk7d5ZG8cFA5nYwIVZBL0RkQ4Fkp2OWsZI9+jVVLWlIbLQnXaSQJUVCsjqEjZ+8vW0D3Y+sxGQghOPnm11JNI2K/H2eDicMvUWcuJoH1WVtTTmE1QGs305vT+1cg98ezbAY7H4zSRSLiz5i47O+DRHhCoXG+ausO4sggAwkrbKZo3EBg5nP6dl6pucO/Yy5In6B0bHn5w5/Zn+sbXM/JHHcKdO4V68ZQ1Xm/rhcS1YXMXVOCAbRUYzJeruaIfIqn3eOuIrAangjM4ZnGn4Rg/y6QPPovhjam3HtbbeEFTJFjfhYB4vmNLFwmKZ4osSdD1XJ+R1e/K9m/8CfBa7gREPgGArkXLW2Wlbpvik8KlTPYhf8B/ka6Xt2z5w7rTTznzw/fIPv+VRqlSVDyekCiryKYzj+14Uf9YZ6ff6O1N2gC4eJSnTiacyNxV3/L52i609NxhyyrcTwlZ5ui8lYOlpaDHr0VaF0miDMfSYeQy+5xK4QuZQ8k1Rx1P+UB766SWBkmWT7MFMQoiRansqyOyAEEqg7tFqzg6dGN63/ofAGA1LuXtOxfRaFRIpVKOR/FeqIW84WIu+1A6kvtMAOEY0UkOAAUhy+C6VFGkUKlYfcV1R+O9Lz+5DgB6e2u1fjS6dJxarXlCJzTvE5/WlKbPOXputFQ+tDsg1QtQJEkQhYikahOIK8Kxy5uKpb4dZtZ8uDK6IQXAgXzq7AmTpk1jmrDYZdLZIqTZkqp5qOiBSqrgrg3DzPY4ZeOpcECfHQ57LyoL2odLw2ftNMrPpMCXiMDbx+LU0qUMqRRhwEcc2+JEp5+KyJGrFL9GK7w4Mnv2knmy6p1s6Pr2ysDBS0whMKNpUl3TqUsv+XY1b2kezXrkxT+Qp1IpOORIKyUw/8rLfGLwPolIjDFTIrICUfDA1cvgnMHixlApU7ihcujnr0s13N4dFL1dN0mC9zOK7AtBEOC6FlzmwNLNPQIpPREJYJXPKzdkh0e+vHf7r26/4orJau+BhRsUXySay4yM5Q/3n3748Ct732jpHJ9XmzSpK9zcMX2PpSNSTBfvap7a9AlQQRscGD4lUuf9rMcfuXpkYPDrDQ1qUJACnxJFGS5zQIkAy9RhmpXflwvCo+TIhk0nX/eaJocm2lYBnLIRx2YDVjX3a8IDU/yRxo8WimOP5Hf98BLE4zS4bt/7ZUecpRDhY5Kv8TTLdGHo1Z2Ck/1v5tBXcwQVd3/hRWB9ddZJl6yqa9AedDljFb30kR3PrX20szPq0+rr13r8wWXZodyBnZt3nQLsHnsbmyYAeHf3uS1i0N/HXKIqqgYIFBU9f2H6oD7U1h550WGWzR277PWFwnpV35IdKd4fqJN+R2GuoqLv04rmVzjYG7k3sfSP54q5OXCz2/Ns93b09+cBoGn+/3kSFMQSqo8jGhXxrWfC6tSZj6paxEfAUMinXzWyozdUh0e3AC+MHFWcxeJybzLx89ldK95XPyHyaYfJP5uxYPFpvVtS2xoaOi9sbp/521Bj3cIFp898tJQOnL9vX6J4JGc+prw1gHEOQRJhm6VXq25pdWXEEJtaJz5GqQCBiZLo0cK2bTGjXBkLBD2HFZF3MqLNlVWvolcMp1ItX3v83Dsep8H7di0LNbQ+ZdpGaTi9pRMDzx8OtH/81mC44augcI1Cdk+6f//5MDcdAAG6T14t9fhe5eNGyIAE7+7uFnt6eticBRduCDbVn2VW8s+/Nrbr7HRvb7mtbUlXy+TwM55gfUO5kHlu87NrzwVQikajYqrWtiVAjEajo6RcnknSzYd9jbq2nzA+smfXpvPaOxZeqXp8cSoF8Fp/ZqOGyo+8YW05pdIqrz8gCaIAcMCyGUyj/FRmwPzmgX2//DU5qoSMjhKkGjminQSphFM365oHAvVtlxUye36c2/2zqydPjoas0Mz9suINm5aZHz7w2lkorN/c0bFc2bdvg/l2ZYtXW9Y0vathjS+kLdEr2f/u2bT2WgBWZ2fnJG/j3I2Sok01q4WeSrW6enfPk5uP+3YnesnzAGZT5uR9wcikSrHCx4rGF/a/8ug3AdgA0NyxvDMS0M4O1QmSqTNkhvmhg/sfeehIckOOQyzw8IQL27xNE3ttQrzZ/Gvz7L612xs6Yl/Wwm1fAKekkE7/KN9/32p0xmT0Jq3xlFYcz5XJ0Q9oIAAw50xf1OVtmfyCIEsKs82ttmPtqhQLD2rBpv8vq+IcgFBuO1mrUr6g/PKeHiEiyE5DeLosNUxUA0KXKJAziSC9H6ASlSgqpeKBzKHsU1U787WO9hal57nUvuM2GzgIVsYokkmXHOdVkBOccemXw3UdXyxWDz2Y3faTyzFpUqjJf/YOjzfUYlqVUv7Q4Gx9mTKCZJK9b8mHlkMNf4c5tsQcUwARQMiR5A5wbRuglBJCOANpIpQKoiRRQigc2wInFIy7nAAQqUQc27ApxQgVZB8hJCSKCqgggLkMuVyGqaoCUZKoY1sjlBIfERUvuAPu2K/YleKXNr/06192dCxXWlv1131D6o1XDscgEZaCITVZ1dTAxTaruvnMobUAeMh3+q2Kxz+BM6Dqlp/Qx9YNdpQ+o+wDzLJBpgU0ZTqjDgRFA2MMIAS8lsODCjIADua6oAQghMAxbXDKACJyxhyXMlfknNkuZRYVZI/ryG2uWx5xXfZ75hj7ywW9Cof2DgwcfGL+/OkzBEl+mKreJrNSzjpm8RFJUuZ7faH5gig9Muukcy7YvW3D+ssvr6Wdb08ijEs50nHJv3kbpv/rWGk4T6lbpo7xmFequ0gU1WbLtayxfOZU++CabUCM1jqUp3nqW+zZWtAiti1BGt/OPsL0A2ACpdRgrGFSwyWBUNvNxUxxY9UY+ZYv2JikgmjbpcyqQnZwv69u4o/rmqcuPrA7/e8H9z582/HUtWvhiutD9ZG7DF3fP3ZoaFVfX+rlRUtWXC2r4bur5VK2MDJ2xr59G/cTQshb+TXxqPp5KZinb8UET6D5GsstcwE2NLW5zdHHPimIokgohVWpvGAfXLP16GTieT0zhM0YOjGZEQicOsBCLbcKHnfOrud/90T3+z98wKt65lKhecv+l54eXVA/9RvVcrbQMmXvHQf3wonFYsLo6Ch5k5qy7u5uoefldf85/7TzA/5w01ea2ie95Ku/aJ1tCGsts7QlUBfo5sy6hRCyejx9ZceW9LiUwydddaff1/a5XOlAVpPlADclgxJRVWSRuIyxdPG1c8z9a1NvSPnIPjF6Yt4KSCaTfNHiD91m25bTMSn47/uHCv+kKH7HeubgT9tj7ewtMfr432yMv0aad9oFt3hV7QtUVgNElODaOgRRhl4u7H950yMdx9qDHInJSCSgdV08P6C2PksYpKpR5ZLPS6leMWTqkQVREYoseyC3+SfTaw+jyN/ohV6cxuNAInHiV4BHSsXOydFmpTU8FZLSoYiMpofYIte2Xjy497F73+Dh3go6tkZAcqUbWXDVer/WdH6pOOzIHo/oGByqJIG73GWiLIyV+z5j7n70PxGNC38xWTfOfBzxqEfU983e9c+6ovixndUJXgfVbDNyUmyZR219mrkms7kjUErBdRuK4ncJFYRyKd2XffW3c8EPGSB//Rcz7/AY57ffsP3GxkZ+vHRWrHUvOn0yrfsvAolYRpHIPj/Mcgma5Bn/IMixXZ5eDQzoWLlSAODi72scF+Bx2YjI5I/cGWid/rmslXFEooqC40B0BQiUuJBVwbDTm9Jb7l3yTjxR/HsY1L/4mhlqpPlTuqMz7nKBCgIooxBEAkYA1zUc0y392xv89P/+QX1V94uCrKm2XoVP1IjETIgCATgcQVQFo1JcX9z2yJOIzSFIJt1/BNCi4IinucTiskIJA0BdCnDCiSDCdcpV3Rz7aq1rsJPjH2SIhIqEAhxc4ZwbhHIZFGCMC2IxM3KLfuhXL6JXE/5RpAwA1HAdiYsSpSCCQFRKRIE6gioWSpmvlg49ege6V0v/SIABQJBaOvMSpDbO+SDn7LAFDGUque8Zr977JcTjBA9+m+EfbPwPDO8Mwje3BFIAAAAASUVORK5CYII=',
'menu_position' => 5,
'supports' => ['editor', 'author', 'comments'],
'has_archive' => false,
diff --git a/includes/class-follows.php b/includes/class-follows.php
new file mode 100644
index 0000000..43fa3e3
--- /dev/null
+++ b/includes/class-follows.php
@@ -0,0 +1,207 @@
+
+ register_rest_route(self::REST_NAMESPACE, '/follow/(?P\d+)', [
+ 'methods' => 'POST',
+ 'callback' => [$this, 'follow_user'],
+ 'permission_callback' => [$this, 'require_login'],
+ ]);
+
+ // Unfollow a user: DELETE /beep/v1/follow/
+ register_rest_route(self::REST_NAMESPACE, '/follow/(?P\d+)', [
+ 'methods' => 'DELETE',
+ 'callback' => [$this, 'unfollow_user'],
+ 'permission_callback' => [$this, 'require_login'],
+ ]);
+
+ // Check follow status: GET /beep/v1/follow/
+ register_rest_route(self::REST_NAMESPACE, '/follow/(?P\d+)', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'check_follow'],
+ 'permission_callback' => '__return_true',
+ ]);
+
+ // Get users I'm following: GET /beep/v1/following
+ register_rest_route(self::REST_NAMESPACE, '/following', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_following'],
+ 'permission_callback' => [$this, 'require_login'],
+ ]);
+
+ // Get my followers: GET /beep/v1/followers
+ register_rest_route(self::REST_NAMESPACE, '/followers', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'get_followers'],
+ 'permission_callback' => [$this, 'require_login'],
+ ]);
+ }
+
+ public function require_login() {
+ return is_user_logged_in() ? true : new WP_Error('rest_not_logged_in', 'You must be logged in', ['status' => 401]);
+ }
+
+ public function follow_user($request) {
+ $user_id = intval($request->get_param('user_id'));
+ $follower_id = get_current_user_id();
+
+ if ($user_id === $follower_id) {
+ return new WP_Error('rest_cannot_follow_self', 'You cannot follow yourself', ['status' => 400]);
+ }
+
+ $target = get_userdata($user_id);
+ if (!$target) {
+ return new WP_Error('rest_user_not_found', 'User not found', ['status' => 404]);
+ }
+
+ global $wpdb;
+ $table = $wpdb->prefix . 'beep_follows';
+
+ $existing = $wpdb->get_row($wpdb->prepare(
+ "SELECT ID FROM `$table` WHERE follower_id = %d AND following_id = %d",
+ $follower_id, $user_id
+ ));
+
+ if ($existing) {
+ return ['status' => 'following', 'user_id' => $user_id, 'display_name' => $target->display_name];
+ }
+
+ $wpdb->insert($table, [
+ 'follower_id' => $follower_id,
+ 'following_id' => $user_id,
+ ], ['%d', '%d']);
+
+ return ['status' => 'following', 'user_id' => $user_id, 'display_name' => $target->display_name];
+ }
+
+ public function unfollow_user($request) {
+ $user_id = intval($request->get_param('user_id'));
+ $follower_id = get_current_user_id();
+
+ global $wpdb;
+ $table = $wpdb->prefix . 'beep_follows';
+
+ $wpdb->delete($table, [
+ 'follower_id' => $follower_id,
+ 'following_id' => $user_id,
+ ], ['%d', '%d']);
+
+ return ['status' => 'unfollowed', 'user_id' => $user_id];
+ }
+
+ public function check_follow($request) {
+ $user_id = intval($request->get_param('user_id'));
+ $current_user = get_current_user_id();
+
+ global $wpdb;
+ $table = $wpdb->prefix . 'beep_follows';
+
+ $is_following = false;
+ if ($current_user) {
+ $is_following = (bool) $wpdb->get_var($wpdb->prepare(
+ "SELECT ID FROM `$table` WHERE follower_id = %d AND following_id = %d",
+ $current_user, $user_id
+ ));
+ }
+
+ $target = get_userdata($user_id);
+ if (!$target) {
+ return new WP_Error('rest_user_not_found', 'User not found', ['status' => 404]);
+ }
+
+ $follower_count = intval($wpdb->get_var($wpdb->prepare(
+ "SELECT COUNT(*) FROM `$table` WHERE following_id = %d", $user_id
+ )));
+
+ $following_count = intval($wpdb->get_var($wpdb->prepare(
+ "SELECT COUNT(*) FROM `$table` WHERE follower_id = %d", $user_id
+ )));
+
+ return [
+ 'is_following' => $is_following,
+ 'user_id' => $user_id,
+ 'display_name' => $target->display_name,
+ 'user_login' => $target->user_login,
+ 'follower_count' => $follower_count,
+ 'following_count' => $following_count,
+ ];
+ }
+
+ public function get_following($request) {
+ $user_id = get_current_user_id();
+
+ global $wpdb;
+ $table = $wpdb->prefix . 'beep_follows';
+
+ $following = $wpdb->get_results($wpdb->prepare(
+ "SELECT u.ID, u.display_name, u.user_login, f.created_at
+ FROM `$table` f
+ JOIN {$wpdb->users} u ON u.ID = f.following_id
+ WHERE f.follower_id = %d
+ ORDER BY f.created_at DESC",
+ $user_id
+ ), ARRAY_A);
+
+ return ['following' => $following, 'count' => count($following)];
+ }
+
+ public function get_followers($request) {
+ $user_id = get_current_user_id();
+
+ global $wpdb;
+ $table = $wpdb->prefix . 'beep_follows';
+
+ $followers = $wpdb->get_results($wpdb->prepare(
+ "SELECT u.ID, u.display_name, u.user_login, f.created_at
+ FROM `$table` f
+ JOIN {$wpdb->users} u ON u.ID = f.follower_id
+ WHERE f.following_id = %d
+ ORDER BY f.created_at DESC",
+ $user_id
+ ), ARRAY_A);
+
+ return ['followers' => $followers, 'count' => count($followers)];
+ }
+
+ public static function is_following($follower_id, $following_id) {
+ global $wpdb;
+ $table = $wpdb->prefix . 'beep_follows';
+ return (bool) $wpdb->get_var($wpdb->prepare(
+ "SELECT ID FROM `$table` WHERE follower_id = %d AND following_id = %d",
+ $follower_id, $following_id
+ ));
+ }
+
+ public static function get_follower_count($user_id) {
+ global $wpdb;
+ $table = $wpdb->prefix . 'beep_follows';
+ return intval($wpdb->get_var($wpdb->prepare(
+ "SELECT COUNT(*) FROM `$table` WHERE following_id = %d", $user_id
+ )));
+ }
+
+ public static function get_following_count($user_id) {
+ global $wpdb;
+ $table = $wpdb->prefix . 'beep_follows';
+ return intval($wpdb->get_var($wpdb->prepare(
+ "SELECT COUNT(*) FROM `$table` WHERE follower_id = %d", $user_id
+ )));
+ }
+}
+
+// Bootstrap — register routes
+add_action('rest_api_init', function() {
+ $f = new Beep_Follows();
+ $f->register_routes();
+});
diff --git a/includes/class-magic-links.php b/includes/class-magic-links.php
new file mode 100644
index 0000000..c1b2f4c
--- /dev/null
+++ b/includes/class-magic-links.php
@@ -0,0 +1,234 @@
+prefix . self::TABLE;
+ if ($wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table) {
+ self::create_table();
+ }
+ }
+
+ public static function create_table() {
+ global $wpdb;
+ $table = $wpdb->prefix . self::TABLE;
+ $charset = $wpdb->get_charset_collate();
+
+ $sql = "CREATE TABLE IF NOT EXISTS {$table} (
+ id bigint(20) unsigned NOT NULL auto_increment,
+ user_id bigint(20) unsigned NOT NULL,
+ token varchar(64) NOT NULL,
+ email varchar(100) NOT NULL,
+ created_at datetime NOT NULL default CURRENT_TIMESTAMP,
+ expires_at datetime NOT NULL,
+ used_at datetime DEFAULT NULL,
+ PRIMARY KEY (id),
+ UNIQUE KEY token (token),
+ KEY user_id (user_id),
+ KEY expires_at (expires_at)
+ ) {$charset};";
+
+ require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
+ dbDelta($sql);
+ }
+
+ public static function register_rest_routes() {
+ register_rest_route('beep/v1', '/magic-link', [
+ 'methods' => 'POST',
+ 'callback' => [__CLASS__, 'request_magic_link'],
+ 'permission_callback' => '__return_true',
+ ]);
+ }
+
+ public static function request_magic_link($request) {
+ $email = sanitize_email($request->get_param('email'));
+
+ if (!$email || !is_email($email)) {
+ return new WP_Error('invalid_email', 'Please enter a valid email address.', ['status' => 400]);
+ }
+
+ $user = get_user_by('email', $email);
+ if (!$user) {
+ // Don't reveal whether an email exists
+ return ['success' => true, 'message' => 'If that email is registered, a login link has been sent.'];
+ }
+
+ // Check rate limit (one request per 5 minutes per email)
+ $rate_key = 'beep_ml_' . md5($email);
+ $last_request = get_transient($rate_key);
+ if ($last_request) {
+ return new WP_Error(
+ 'rate_limited',
+ 'Please wait a few minutes before requesting another login link.',
+ ['status' => 429]
+ );
+ }
+
+ // Generate token
+ $token = wp_generate_password(self::TOKEN_LENGTH, false);
+ $hashed_token = hash('sha256', $token);
+ $expires_at = date('Y-m-d H:i:s', time() + (self::EXPIRY_HOURS * 3600));
+
+ // Store in database
+ global $wpdb;
+ $table = $wpdb->prefix . self::TABLE;
+
+ $wpdb->insert($table, [
+ 'user_id' => $user->ID,
+ 'token' => $hashed_token,
+ 'email' => $email,
+ 'expires_at' => $expires_at,
+ ]);
+
+ // Set rate limit
+ set_transient($rate_key, true, 5 * 60);
+
+ // Build magic link
+ $login_url = add_query_arg([
+ 'beep_magic' => $token,
+ 'uid' => $user->ID,
+ ], home_url('/'));
+
+ // Email content
+ $site_name = get_bloginfo('name');
+ $subject = sprintf('[%s] Your login link', $site_name);
+ $message = sprintf(
+ "Hi %s,\n\nClick the link below to sign in to %s:\n\n%s\n\nThis link expires in %d hours.\n\nIf you didn't request this, you can ignore this email.\n\n— The %s team",
+ $user->display_name,
+ $site_name,
+ $login_url,
+ self::EXPIRY_HOURS,
+ $site_name
+ );
+
+ // Send email
+ $sent = wp_mail($email, $subject, $message);
+
+ if (!$sent) {
+ return new WP_Error('email_failed', 'Failed to send email. Please try again.', ['status' => 500]);
+ }
+
+ return ['success' => true, 'message' => 'Check your email for the login link!'];
+ }
+
+ public static function handle_magic_link_login() {
+ if (!isset($_GET['beep_magic']) || !isset($_GET['uid'])) {
+ return;
+ }
+
+ $token = sanitize_text_field($_GET['beep_magic']);
+ $user_id = (int) $_GET['uid'];
+
+ if (!$token || !$user_id) {
+ return;
+ }
+
+ // Verify token
+ global $wpdb;
+ $table = $wpdb->prefix . self::TABLE;
+ $hashed_token = hash('sha256', $token);
+
+ $record = $wpdb->get_row($wpdb->prepare(
+ "SELECT * FROM {$table} WHERE token = %s AND user_id = %d AND used_at IS NULL AND expires_at > %s",
+ $hashed_token,
+ $user_id,
+ current_time('mysql')
+ ));
+
+ if (!$record) {
+ wp_die('This login link is invalid or has expired. Please request a new one.');
+ }
+
+ // Mark as used
+ $wpdb->update(
+ $table,
+ ['used_at' => current_time('mysql')],
+ ['id' => $record->id]
+ );
+
+ // Log the user in
+ $user = get_user_by('id', $user_id);
+ if ($user) {
+ wp_set_current_user($user_id);
+ wp_set_auth_cookie($user_id);
+ do_action('wp_login', $user->user_login, $user);
+
+ // Redirect to home or beep page
+ $redirect_url = isset($_GET['redirect_to']) ? esc_url_raw($_GET['redirect_to']) : home_url('/');
+ wp_redirect($redirect_url);
+ exit;
+ }
+ }
+
+ public static function cleanup_expired_tokens() {
+ // Run once per session
+ static $run = false;
+ if ($run) {
+ return;
+ }
+ $run = true;
+
+ global $wpdb;
+ $table = $wpdb->prefix . self::TABLE;
+ $wpdb->query($wpdb->prepare(
+ "DELETE FROM {$table} WHERE expires_at < %s OR (used_at IS NOT NULL AND DATE_ADD(used_at, INTERVAL 1 HOUR) < %s)",
+ current_time('mysql'),
+ current_time('mysql')
+ ));
+ }
+
+ public static function render_login_form() {
+ ob_start();
+ ?>
+
+
+
+
+
+
+
Check your email for the login link!
+
+
+
+
+ 'POST',
'callback' => [__CLASS__, 'submit_beep'],
- 'permission_callback' => ['Beep_Likes', 'can_interact'],
+ 'permission_callback' => [Beep_Likes::class, 'can_interact'],
]);
+
register_rest_route('beep/v1', '/reply/(?P\d+)', [
'methods' => 'POST',
'callback' => [__CLASS__, 'submit_reply'],
- 'permission_callback' => ['Beep_Likes', 'can_interact'],
+ 'permission_callback' => [Beep_Likes::class, '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+)', [
+
+ register_rest_route('beep/v1', '/beep/(?P\d+)', [
'methods' => 'DELETE',
'callback' => [__CLASS__, 'delete_beep'],
- 'permission_callback' => ['Beep_Likes', 'can_interact'],
+ 'permission_callback' => function ($req) {
+ return current_user_can('delete_post', (int) $req['id']);
+ },
]);
+
register_rest_route('beep/v1', '/reply/(?P\d+)', [
'methods' => 'DELETE',
'callback' => [__CLASS__, 'delete_reply'],
- 'permission_callback' => ['Beep_Likes', 'can_interact'],
+ 'permission_callback' => function () {
+ return current_user_can('moderate_comments');
+ },
]);
}
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]);
+ $content = trim((string) $request->get_param('content'));
+ $length = function_exists('mb_strlen') ? mb_strlen($content) : strlen($content);
+
+ if ($content === '' || $length > BEEP_CHAR_LIMIT) {
+ return new WP_Error('beep_invalid_content',
+ 'Content must be 1-' . BEEP_CHAR_LIMIT . ' characters.',
+ ['status' => 400]);
}
+
$post_id = wp_insert_post([
- 'post_type' => 'beep',
- 'post_content' => $content,
+ 'post_type' => Beep_CPT::POST_TYPE,
'post_status' => 'publish',
'post_author' => get_current_user_id(),
+ 'post_content' => wp_kses($content, []),
+ 'post_title' => wp_trim_words($content, 8, '...'),
+ ], true);
+
+ if (is_wp_error($post_id)) {
+ return $post_id;
+ }
+
+ return rest_ensure_response([
+ 'id' => $post_id,
+ 'html' => beep_render_post(get_post($post_id)),
]);
- if (is_wp_error($post_id)) { return $post_id; }
- 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]);
+ $post_id = (int) $request['id'];
+ $content = trim((string) $request->get_param('content'));
+ $parent_id = (int) $request->get_param('parent');
+ $length = function_exists('mb_strlen') ? mb_strlen($content) : strlen($content);
+
+ if (get_post_type($post_id) !== Beep_CPT::POST_TYPE) {
+ return new WP_Error('beep_not_a_beep', 'Not a beep.', ['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]);
+ if ($content === '' || $length > BEEP_CHAR_LIMIT) {
+ return new WP_Error('beep_invalid_content',
+ 'Reply must be 1-' . BEEP_CHAR_LIMIT . ' characters.',
+ ['status' => 400]);
}
+ // Validate the parent comment belongs to this post if provided.
+ if ($parent_id > 0) {
+ $parent = get_comment($parent_id);
+ if (!$parent || (int) $parent->comment_post_ID !== $post_id) {
+ return new WP_Error('beep_bad_parent', 'Invalid parent.', ['status' => 400]);
+ }
+ }
+
+ $user = wp_get_current_user();
+
+ $approved = get_option('comment_moderation') ? 0 : 1;
+ if (get_option('comment_previously_approved')) {
+ $prev = get_comments([
+ 'user_id' => $user->ID,
+ 'status' => 'approve',
+ 'count' => true,
+ ]);
+ if (!$prev) {
+ $approved = 0;
+ }
+ }
+
$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(),
+ 'comment_post_ID' => $post_id,
+ 'comment_author' => $user->display_name,
+ 'comment_author_email' => $user->user_email,
+ 'comment_author_url' => '',
+ 'comment_content' => wp_kses($content, []),
+ 'comment_type' => 'comment',
+ 'comment_parent' => $parent_id,
+ 'user_id' => $user->ID,
+ 'comment_approved' => $approved,
]);
+
if (!$comment_id) {
- return new WP_Error('insert_failed', 'Could not save reply.', ['status' => 500]);
+ return new WP_Error('beep_insert_failed', 'Reply could not be saved.', ['status' => 500]);
}
- return rest_ensure_response(['id' => $comment_id]);
+
+ $comment = get_comment($comment_id);
+ $pending = ($comment->comment_approved == 0);
+
+ // Depth of this reply for client-side indenting.
+ $depth = 0;
+ $cur = $comment;
+ while ($cur && (int) $cur->comment_parent > 0 && $depth < 10) {
+ $cur = get_comment((int) $cur->comment_parent);
+ $depth++;
+ }
+ $can_interact = Beep_Likes::can_interact();
+
+ return rest_ensure_response([
+ 'id' => $comment_id,
+ 'parent' => (int) $comment->comment_parent,
+ 'pending' => $pending,
+ 'html' => $pending ? '' : beep_render_reply($comment, [], $depth, $post_id, $can_interact),
+ ]);
}
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 (get_post_type($post_id) !== Beep_CPT::POST_TYPE) {
+ return new WP_Error('beep_not_a_beep', 'Not a beep.', ['status' => 400]);
}
$deleted = wp_delete_post($post_id, true);
if (!$deleted) {
- return new WP_Error('delete_failed', 'Could not delete.', ['status' => 500]);
+ return new WP_Error('beep_delete_failed', 'Could not delete.', ['status' => 500]);
}
return rest_ensure_response(['deleted' => true]);
}
@@ -90,7 +155,7 @@ class Beep_Replies {
$comment_id = (int) $request['id'];
$deleted = wp_delete_comment($comment_id, true);
if (!$deleted) {
- return new WP_Error('delete_failed', 'Could not delete.', ['status' => 500]);
+ return new WP_Error('beep_delete_failed', 'Could not delete.', ['status' => 500]);
}
return rest_ensure_response(['deleted' => true]);
}
@@ -107,110 +172,4 @@ class Beep_Replies {
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-shortcode.php b/includes/class-shortcode.php
index a0653c8..466ca04 100755
--- a/includes/class-shortcode.php
+++ b/includes/class-shortcode.php
@@ -86,6 +86,11 @@ class Beep_Shortcode {
$posts = ($sort === 'top') ? self::get_top_beeps($limit) : self::get_latest_beeps($limit);
$has_posts = !empty($posts);
+ // Show magic login form if not logged in
+ if (!is_user_logged_in()) {
+ return Beep_Magic_Links::render_login_form() . beep_render_public_feed($posts, $atts);
+ }
+
ob_start();
?>
@@ -135,7 +140,19 @@ class Beep_Shortcode {
var BeepConfig=%s;',
+ json_encode([
+ '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(),
+ ])
+ );
+ $beep_script = '';
+ return ob_get_clean() . $beep_config . $beep_script . beep_render_thread_modal() . beep_render_gif_modal();
}
/**
diff --git a/includes/create_poll_tables.php b/includes/create_poll_tables.php
new file mode 100644
index 0000000..3ed8028
--- /dev/null
+++ b/includes/create_poll_tables.php
@@ -0,0 +1,56 @@
+prefix . "beep_polls";
+$votes_table = $wpdb->prefix . "beep_poll_votes";
+$charset = $wpdb->get_charset_collate();
+
+$polls_sql = "CREATE TABLE IF NOT EXISTS $polls_table (
+ 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 IF NOT EXISTS $votes_table (
+ 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");
+$result1 = dbDelta($polls_sql);
+$result2 = dbDelta($votes_sql);
+echo "Tables created: $polls_table, $votes_table";
+echo "
+Result1: " . print_r($result1, true);
+echo "
+Result2: " . print_r($result2, true);
+?>
\ No newline at end of file
diff --git a/includes/helpers.php b/includes/helpers.php
index 8a911bb..9a620aa 100755
--- a/includes/helpers.php
+++ b/includes/helpers.php
@@ -7,8 +7,8 @@ if (!defined('ABSPATH')) {
* Wordmark (bird + "Beep!") shown at the top of the feed.
*/
function beep_logo_svg() {
- $src = esc_url(BEEP_URL . 'assets/beep-wordmark.png');
- return '
';
+ $src = esc_url(BEEP_URL . 'assets/beep-wordmark.jpg');
+ return 'Messages from
';
}
/**
@@ -241,6 +241,12 @@ function beep_render_post($post) {
0 ? esc_html($reply_count) : ''; ?>
+
-
-
-
+
-
-
-
' .
'';
}
+
+
+function beep_render_public_feed($posts, $atts) {
+ ob_start();
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
No beeps yet.
+
+
+