···3737export { default as VideoRetry } from "./components/mobile-player/video-retry";
3838export * from "./lib/system-messages";
39394040+export * from "./components/stream-notification";
4141+4042export * from "./utils/format-handle";
41434244export { DanmuOverlay } from "./components/danmu/danmu-overlay";
···77 PlaceStreamChatMessage,
88 PlaceStreamDefs,
99 PlaceStreamLivestream,
1010+ PlaceStreamLiveTeleport,
1011 PlaceStreamModerationPermission,
1112 PlaceStreamSegment,
1213} from "streamplace";
···121122 pendingHides: newPendingHides,
122123 };
123124 state = reduceChat(state, [], [], [hiddenMessageUri]);
124124- } else if (
125125- PlaceStreamModerationPermission.isRecord(message) ||
126126- (message &&
127127- typeof message === "object" &&
128128- "$type" in message &&
129129- (message as { $type?: string }).$type ===
130130- "place.stream.moderation.permission")
131131- ) {
132132- // Handle moderation permission record updates
133133- // This can be a new permission or a deletion marker
134134- const permRecord = message as
135135- | PlaceStreamModerationPermission.Record
136136- | { deleted?: boolean; rkey?: string; streamer?: string };
137137-138138- if ((permRecord as any).deleted) {
139139- // Handle deletion: clear permissions to trigger refetch
140140- // The useCanModerate hook will refetch and repopulate
141141- state = {
142142- ...state,
143143- moderationPermissions: [],
144144- };
145145- } else {
146146- // Handle new/updated permission: add or update in the list
147147- // Use createdAt as a unique identifier since multiple records can exist for the same moderator
148148- // (e.g., one record with "ban" permission, another with "hide" permission)
149149- // Note: rkey would be ideal but isn't always present in the WebSocket message
150150- const newPerm =
151151- permRecord as PlaceStreamModerationPermission.Record & {
152152- rkey?: string;
153153- };
154154- const existingIndex = state.moderationPermissions.findIndex((p) => {
155155- const pWithRkey = p as PlaceStreamModerationPermission.Record & {
156156- rkey?: string;
157157- };
158158- // Prefer matching by rkey if available, fall back to createdAt
159159- if (newPerm.rkey && pWithRkey.rkey) {
160160- return pWithRkey.rkey === newPerm.rkey;
161161- }
162162- return (
163163- p.moderator === newPerm.moderator &&
164164- p.createdAt === newPerm.createdAt
165165- );
166166- });
167167-168168- let newPermissions: PlaceStreamModerationPermission.Record[];
169169- if (existingIndex >= 0) {
170170- // Update existing record with same moderator AND createdAt
171171- newPermissions = [...state.moderationPermissions];
172172- newPermissions[existingIndex] = newPerm;
173173- } else {
174174- // Add new record (could be a new record for an existing moderator with different permissions)
175175- newPermissions = [...state.moderationPermissions, newPerm];
176176- }
177177-178178- state = {
179179- ...state,
180180- moderationPermissions: newPermissions,
181181- };
182182- }
183125 }
184126 }
185127 }
+1
js/components/src/streamplace-store/index.tsx
···55export * from "./stream";
66export * from "./streamplace-store";
77export * from "./user";
88+export * from "./xrpc";
+33
lexicons/place/stream/live/teleport.json
···11+{
22+ "lexicon": 1,
33+ "id": "place.stream.live.teleport",
44+ "defs": {
55+ "main": {
66+ "type": "record",
77+ "key": "tid",
88+ "description": "Record defining a 'teleport', that is active during a certain time.",
99+ "record": {
1010+ "type": "object",
1111+ "required": ["streamer", "startsAt"],
1212+ "properties": {
1313+ "streamer": {
1414+ "type": "string",
1515+ "format": "did",
1616+ "description": "The DID of the streamer to teleport to."
1717+ },
1818+ "startsAt": {
1919+ "type": "string",
2020+ "format": "datetime",
2121+ "description": "The time the teleport becomes active."
2222+ },
2323+ "durationSeconds": {
2424+ "type": "integer",
2525+ "description": "The time limit in seconds for the teleport. If not set, the teleport is permanent. Must be at least 60 seconds, and no more than 32,400 seconds (9 hours).",
2626+ "minimum": 60,
2727+ "maximum": 32400
2828+ }
2929+ }
3030+ }
3131+ }
3232+ }
3333+}
···5526552655275527 return nil
55285528}
55295529-func (t *LiveRecommendations) MarshalCBOR(w io.Writer) error {
55295529+func (t *LiveTeleport) MarshalCBOR(w io.Writer) error {
55305530 if t == nil {
55315531 _, err := w.Write(cbg.CborNull)
55325532 return err
55335533 }
5534553455355535 cw := cbg.NewCborWriter(w)
55365536+ fieldCount := 4
5536553755375537- if _, err := cw.Write([]byte{163}); err != nil {
55385538+ if t.DurationSeconds == nil {
55395539+ fieldCount--
55405540+ }
55415541+55425542+ if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
55385543 return err
55395544 }
55405545···55505555 return err
55515556 }
5552555755535553- if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("place.stream.live.recommendations"))); err != nil {
55585558+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("place.stream.live.teleport"))); err != nil {
55545559 return err
55555560 }
55565556- if _, err := cw.WriteString(string("place.stream.live.recommendations")); err != nil {
55615561+ if _, err := cw.WriteString(string("place.stream.live.teleport")); err != nil {
55575562 return err
55585563 }
5559556455605560- // t.CreatedAt (string) (string)
55615561- if len("createdAt") > 1000000 {
55625562- return xerrors.Errorf("Value in field \"createdAt\" was too long")
55655565+ // t.StartsAt (string) (string)
55665566+ if len("startsAt") > 1000000 {
55675567+ return xerrors.Errorf("Value in field \"startsAt\" was too long")
55635568 }
5564556955655565- if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil {
55705570+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("startsAt"))); err != nil {
55665571 return err
55675572 }
55685568- if _, err := cw.WriteString(string("createdAt")); err != nil {
55735573+ if _, err := cw.WriteString(string("startsAt")); err != nil {
55695574 return err
55705575 }
5571557655725572- if len(t.CreatedAt) > 1000000 {
55735573- return xerrors.Errorf("Value in field t.CreatedAt was too long")
55775577+ if len(t.StartsAt) > 1000000 {
55785578+ return xerrors.Errorf("Value in field t.StartsAt was too long")
55745579 }
5575558055765576- if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil {
55815581+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.StartsAt))); err != nil {
55775582 return err
55785583 }
55795579- if _, err := cw.WriteString(string(t.CreatedAt)); err != nil {
55845584+ if _, err := cw.WriteString(string(t.StartsAt)); err != nil {
55805585 return err
55815586 }
5582558755835583- // t.Streamers ([]string) (slice)
55845584- if len("streamers") > 1000000 {
55855585- return xerrors.Errorf("Value in field \"streamers\" was too long")
55885588+ // t.Streamer (string) (string)
55895589+ if len("streamer") > 1000000 {
55905590+ return xerrors.Errorf("Value in field \"streamer\" was too long")
55865591 }
5587559255885588- if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("streamers"))); err != nil {
55935593+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("streamer"))); err != nil {
55895594 return err
55905595 }
55915591- if _, err := cw.WriteString(string("streamers")); err != nil {
55965596+ if _, err := cw.WriteString(string("streamer")); err != nil {
55925597 return err
55935598 }
5594559955955595- if len(t.Streamers) > 8192 {
55965596- return xerrors.Errorf("Slice value in field t.Streamers was too long")
56005600+ if len(t.Streamer) > 1000000 {
56015601+ return xerrors.Errorf("Value in field t.Streamer was too long")
55975602 }
5598560355995599- if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Streamers))); err != nil {
56045604+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Streamer))); err != nil {
56005605 return err
56015606 }
56025602- for _, v := range t.Streamers {
56035603- if len(v) > 1000000 {
56045604- return xerrors.Errorf("Value in field v was too long")
56075607+ if _, err := cw.WriteString(string(t.Streamer)); err != nil {
56085608+ return err
56095609+ }
56105610+56115611+ // t.DurationSeconds (int64) (int64)
56125612+ if t.DurationSeconds != nil {
56135613+56145614+ if len("durationSeconds") > 1000000 {
56155615+ return xerrors.Errorf("Value in field \"durationSeconds\" was too long")
56055616 }
5606561756075607- if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
56185618+ if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("durationSeconds"))); err != nil {
56085619 return err
56095620 }
56105610- if _, err := cw.WriteString(string(v)); err != nil {
56215621+ if _, err := cw.WriteString(string("durationSeconds")); err != nil {
56115622 return err
56125623 }
5613562456255625+ if t.DurationSeconds == nil {
56265626+ if _, err := cw.Write(cbg.CborNull); err != nil {
56275627+ return err
56285628+ }
56295629+ } else {
56305630+ if *t.DurationSeconds >= 0 {
56315631+ if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(*t.DurationSeconds)); err != nil {
56325632+ return err
56335633+ }
56345634+ } else {
56355635+ if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-*t.DurationSeconds-1)); err != nil {
56365636+ return err
56375637+ }
56385638+ }
56395639+ }
56405640+56145641 }
56155642 return nil
56165643}
5617564456185618-func (t *LiveRecommendations) UnmarshalCBOR(r io.Reader) (err error) {
56195619- *t = LiveRecommendations{}
56455645+func (t *LiveTeleport) UnmarshalCBOR(r io.Reader) (err error) {
56465646+ *t = LiveTeleport{}
5620564756215648 cr := cbg.NewCborReader(r)
56225649···56355662 }
5636566356375664 if extra > cbg.MaxLength {
56385638- return fmt.Errorf("LiveRecommendations: map struct too large (%d)", extra)
56655665+ return fmt.Errorf("LiveTeleport: map struct too large (%d)", extra)
56395666 }
5640566756415668 n := extra
5642566956435643- nameBuf := make([]byte, 9)
56705670+ nameBuf := make([]byte, 15)
56445671 for i := uint64(0); i < n; i++ {
56455672 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
56465673 if err != nil {
···5667569456685695 t.LexiconTypeID = string(sval)
56695696 }
56705670- // t.CreatedAt (string) (string)
56715671- case "createdAt":
56975697+ // t.StartsAt (string) (string)
56985698+ case "startsAt":
5672569956735700 {
56745701 sval, err := cbg.ReadStringWithMax(cr, 1000000)
···56765703 return err
56775704 }
5678570556795679- t.CreatedAt = string(sval)
57065706+ t.StartsAt = string(sval)
56805707 }
56815681- // t.Streamers ([]string) (slice)
56825682- case "streamers":
57085708+ // t.Streamer (string) (string)
57095709+ case "streamer":
5683571056845684- maj, extra, err = cr.ReadHeader()
56855685- if err != nil {
56865686- return err
56875687- }
57115711+ {
57125712+ sval, err := cbg.ReadStringWithMax(cr, 1000000)
57135713+ if err != nil {
57145714+ return err
57155715+ }
5688571656895689- if extra > 8192 {
56905690- return fmt.Errorf("t.Streamers: array too large (%d)", extra)
57175717+ t.Streamer = string(sval)
56915718 }
57195719+ // t.DurationSeconds (int64) (int64)
57205720+ case "durationSeconds":
57215721+ {
5692572256935693- if maj != cbg.MajArray {
56945694- return fmt.Errorf("expected cbor array")
56955695- }
56965696-56975697- if extra > 0 {
56985698- t.Streamers = make([]string, extra)
56995699- }
57005700-57015701- for i := 0; i < int(extra); i++ {
57025702- {
57035703- var maj byte
57045704- var extra uint64
57055705- var err error
57065706- _ = maj
57075707- _ = extra
57085708- _ = err
57095709-57105710- {
57115711- sval, err := cbg.ReadStringWithMax(cr, 1000000)
57125712- if err != nil {
57135713- return err
57235723+ b, err := cr.ReadByte()
57245724+ if err != nil {
57255725+ return err
57265726+ }
57275727+ if b != cbg.CborNull[0] {
57285728+ if err := cr.UnreadByte(); err != nil {
57295729+ return err
57305730+ }
57315731+ maj, extra, err := cr.ReadHeader()
57325732+ if err != nil {
57335733+ return err
57345734+ }
57355735+ var extraI int64
57365736+ switch maj {
57375737+ case cbg.MajUnsignedInt:
57385738+ extraI = int64(extra)
57395739+ if extraI < 0 {
57405740+ return fmt.Errorf("int64 positive overflow")
57145741 }
57155715-57165716- t.Streamers[i] = string(sval)
57425742+ case cbg.MajNegativeInt:
57435743+ extraI = int64(extra)
57445744+ if extraI < 0 {
57455745+ return fmt.Errorf("int64 negative overflow")
57465746+ }
57475747+ extraI = -1 - extraI
57485748+ default:
57495749+ return fmt.Errorf("wrong type for int64 field: %d", maj)
57175750 }
5718575157525752+ t.DurationSeconds = (*int64)(&extraI)
57195753 }
57205754 }
57215755
+23
pkg/streamplace/liveteleport.go
···11+// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
22+33+package streamplace
44+55+// schema: place.stream.live.teleport
66+77+import (
88+ "github.com/bluesky-social/indigo/lex/util"
99+)
1010+1111+func init() {
1212+ util.RegisterType("place.stream.live.teleport", &LiveTeleport{})
1313+} //
1414+// RECORDTYPE: LiveTeleport
1515+type LiveTeleport struct {
1616+ LexiconTypeID string `json:"$type,const=place.stream.live.teleport" cborgen:"$type,const=place.stream.live.teleport"`
1717+ // durationSeconds: The time limit in seconds for the teleport. If not set, the teleport is permanent. Must be at least 60 seconds, and no more than 32,400 seconds (9 hours).
1818+ DurationSeconds *int64 `json:"durationSeconds,omitempty" cborgen:"durationSeconds,omitempty"`
1919+ // startsAt: The time the teleport becomes active.
2020+ StartsAt string `json:"startsAt" cborgen:"startsAt"`
2121+ // streamer: The DID of the streamer to teleport to.
2222+ Streamer string `json:"streamer" cborgen:"streamer"`
2323+}