diff --git a/js/app/src/screens/chat-popout.tsx b/js/app/src/screens/chat-popout.tsx
index 382da0b5..49622165 100644
--- a/js/app/src/screens/chat-popout.tsx
+++ b/js/app/src/screens/chat-popout.tsx
@@ -22,6 +22,7 @@ interface ChatPopoutParams {
reverse?: string;
hideAfter?: string;
hideChatBox?: string;
+ hidePinnedComments?: string;
showNotifications?: string;
}
@@ -65,7 +66,7 @@ export function PopoutChatInner({ params }: { params: ChatPopoutParams }) {
? parseInt(params.hideAfter, 10)
: undefined;
const hideChatBox = params.hideChatBox === "true";
- const showNotifications = params.showNotifications === "true";
+ const hidePinnedComments = params.hidePinnedComments === "true";
useEffect(() => {
setSrc(params.user);
@@ -88,7 +89,7 @@ export function PopoutChatInner({ params }: { params: ChatPopoutParams }) {
{ maxHeight: "100vh" },
]}
>
- {showNotifications ? (
+ {!hidePinnedComments ? (
{chat}
diff --git a/js/components/src/components/dashboard/moderator-panel.tsx b/js/components/src/components/dashboard/moderator-panel.tsx
index f13ce7d2..4366cb66 100644
--- a/js/components/src/components/dashboard/moderator-panel.tsx
+++ b/js/components/src/components/dashboard/moderator-panel.tsx
@@ -596,6 +596,53 @@ function AddModeratorDialog({
+
+
+ setPermissions((p) => ({
+ ...p,
+ "message.pin": !p["message.pin"],
+ }))
+ }
+ style={[
+ layout.flex.row,
+ layout.flex.alignCenter,
+ p[3],
+ r.md,
+ bg.neutral[800],
+ borders.width.thin,
+ borders.color.neutral[700],
+ ]}
+ >
+
+ {permissions["message.pin"] && (
+ ✓
+ )}
+
+
+
+ Pin Messages
+
+
+ Pin and unpin chat messages
+
+
+
diff --git a/js/docs/src/content/docs/features/chat-popout.md b/js/docs/src/content/docs/features/chat-popout.md
index b8b120d0..d071e85a 100644
--- a/js/docs/src/content/docs/features/chat-popout.md
+++ b/js/docs/src/content/docs/features/chat-popout.md
@@ -63,3 +63,16 @@ Example:
```
https://stream.place/chat-popout/did:plc:abcdef123456...?hideChatBox=true
```
+
+### `hidePinnedComments`
+
+Hides pinned comment notifications in the popout.
+
+- Type: `boolean` (values: `"true"`, `"false"`)
+- Default: `"false"`
+
+Example:
+
+```
+https://stream.place/chat-popout/did:plc:abcdef123456...?hidePinnedComments=true
+```
diff --git a/pkg/spxrpc/place_stream_moderation.go b/pkg/spxrpc/place_stream_moderation.go
index f621f8e3..a70ff81d 100644
--- a/pkg/spxrpc/place_stream_moderation.go
+++ b/pkg/spxrpc/place_stream_moderation.go
@@ -376,25 +376,6 @@ func (s *Server) handlePlaceStreamModerationCreatePin(ctx context.Context, input
return nil, err
}
- // Check that the streamer has an active livestream
- ls, err := s.model.GetLatestLivestreamForRepo(input.Streamer)
- if err != nil {
- log.Error(ctx, "failed to get livestream for streamer", "err", err)
- return nil, echo.NewHTTPError(http.StatusInternalServerError, "failed to check livestream status")
- }
- if ls == nil || ls.Livestream == nil {
- return nil, echo.NewHTTPError(http.StatusBadRequest, "no active livestream for this streamer")
- }
- lsRec, err := lexutil.CborDecodeValue(*ls.Livestream)
- if err != nil {
- log.Error(ctx, "failed to decode livestream record", "err", err)
- return nil, echo.NewHTTPError(http.StatusInternalServerError, "failed to check livestream status")
- }
- lsTyped, ok := lsRec.(*streamplace.Livestream)
- if ok && lsTyped.EndedAt != nil {
- return nil, echo.NewHTTPError(http.StatusBadRequest, "livestream has ended, cannot pin comments")
- }
-
// Delete existing pinned records for this streamer (single-pin semantics)
var listOutput comatproto.RepoListRecords_Output
err = modCtx.StreamerClient.Do(ctx, xrpc.Query, "application/json", "com.atproto.repo.listRecords",