Live video on the AT Protocol

Compare changes

Choose any two refs to compare.

+229 -580
+1 -1
.prettierrc
··· 14 14 } 15 15 } 16 16 ], 17 - "plugins": ["prettier-plugin-organize-imports", "prettier-plugin-curly"] 17 + "plugins": ["prettier-plugin-organize-imports"] 18 18 }
+5 -15
js/app/components/container.tsx
··· 18 18 } 19 19 20 20 function getMaxWidth(width: number): number { 21 - if (width >= 1660) { 22 - return maxContainerWidths.threeXl; 23 - } 24 - if (width >= 1260) { 25 - return maxContainerWidths.twoXl; 26 - } 27 - if (width >= 800) { 28 - return maxContainerWidths.xl; 29 - } 30 - if (width >= 740) { 31 - return maxContainerWidths.lg; 32 - } 33 - if (width >= 660) { 34 - return maxContainerWidths.md; 35 - } 21 + if (width >= 1660) return maxContainerWidths.threeXl; 22 + if (width >= 1260) return maxContainerWidths.twoXl; 23 + if (width >= 800) return maxContainerWidths.xl; 24 + if (width >= 740) return maxContainerWidths.lg; 25 + if (width >= 660) return maxContainerWidths.md; 36 26 return maxContainerWidths.sm; 37 27 } 38 28
+5 -11
js/app/components/debug/breakpoint-indicator.tsx
··· 6 6 7 7 // Simple breakpoint detection based on width 8 8 let current = "xs"; 9 - if (width >= 1536) { 10 - current = "xxl"; 11 - } else if (width >= 1280) { 12 - current = "xl"; 13 - } else if (width >= 1024) { 14 - current = "lg"; 15 - } else if (width >= 768) { 16 - current = "md"; 17 - } else if (width >= 640) { 18 - current = "sm"; 19 - } 9 + if (width >= 1536) current = "xxl"; 10 + else if (width >= 1280) current = "xl"; 11 + else if (width >= 1024) current = "lg"; 12 + else if (width >= 768) current = "md"; 13 + else if (width >= 640) current = "sm"; 20 14 21 15 return ( 22 16 <View
+1 -3
js/app/components/emoji-picker/emoji-picker.tsx
··· 12 12 } 13 13 14 14 export function EmojiPicker({ isOpen, onClose }: EmojiPickerProps) { 15 - if (!isOpen) { 16 - return null; 17 - } 15 + if (!isOpen) return null; 18 16 19 17 const isWeb = Platform.OS === "web"; 20 18
+4 -12
js/app/components/follow-button.tsx
··· 32 32 const unfollowUser = useStore((state) => state.unfollowUser); 33 33 34 34 // Hide button if not logged in or viewing own stream 35 - if (!currentUserDID || currentUserDID === streamerDID) { 36 - return null; 37 - } 35 + if (!currentUserDID || currentUserDID === streamerDID) return null; 38 36 39 37 // Fetch initial follow state using xrpc 40 38 useEffect(() => { 41 39 let cancelled = false; 42 40 43 41 const fetchFollowStatus = async () => { 44 - if (!currentUserDID || !streamerDID) { 45 - return; 46 - } 42 + if (!currentUserDID || !streamerDID) return; 47 43 48 44 setError(null); 49 45 try { ··· 60 56 } 61 57 62 58 const data = await res.json(); 63 - if (cancelled) { 64 - return; 65 - } 59 + if (cancelled) return; 66 60 67 61 if (data.follow) { 68 62 setIsFollowing(true); ··· 72 66 setFollowUri(null); 73 67 } 74 68 } catch (err) { 75 - if (!cancelled) { 76 - setError("Could not determine follow state"); 77 - } 69 + if (!cancelled) setError("Could not determine follow state"); 78 70 } 79 71 }; 80 72
+3 -9
js/app/components/live-dashboard/bento-grid.tsx
··· 87 87 88 88 // Calculate uptime 89 89 const getUptime = useCallback((): string => { 90 - if (!ingestStarted || !isLive) { 91 - return "00:00:00"; 92 - } 90 + if (!ingestStarted || !isLive) return "00:00:00"; 93 91 const uptimeMs = Date.now() - ingestStarted; 94 92 const seconds = Math.floor(uptimeMs / 1000); 95 93 const hours = Math.floor(seconds / 3600); ··· 100 98 101 99 // Calculate bitrate 102 100 const getBitrate = useCallback((): string => { 103 - if (!seg?.size || !seg?.duration) { 104 - return "0 kbps"; 105 - } 101 + if (!seg?.size || !seg?.duration) return "0 kbps"; 106 102 const kbps = 107 103 (seg.size * 8) / 108 104 ((seg.duration || 1000000000) / 1000000000) / ··· 117 113 | "good" 118 114 | "poor" 119 115 | "offline" => { 120 - if (!isLive) { 121 - return "offline"; 122 - } 116 + if (!isLive) return "offline"; 123 117 switch (segmentTiming.connectionQuality) { 124 118 case "good": 125 119 return "excellent";
+4 -12
js/app/components/live-dashboard/livestream-panel.tsx
··· 96 96 onGoToMetadata?: () => void; 97 97 }) => { 98 98 const imageUrl = useMemo(() => { 99 - if (!selectedImage) { 100 - return undefined; 101 - } 99 + if (!selectedImage) return undefined; 102 100 if (selectedImage instanceof File || selectedImage instanceof Blob) { 103 101 return URL.createObjectURL(selectedImage); 104 102 } ··· 270 268 ); 271 269 272 270 const handleSubmit = useCallback(async () => { 273 - if (!title.trim()) { 274 - return; 275 - } 271 + if (!title.trim()) return; 276 272 277 273 setLoading(true); 278 274 ··· 372 368 }, []); 373 369 374 370 const handleUseLastImage = useCallback(async () => { 375 - if (!livestream?.record.thumb) { 376 - return; 377 - } 371 + if (!livestream?.record.thumb) return; 378 372 379 373 try { 380 374 const did = livestream.uri.split("/")[2]; ··· 397 391 ); 398 392 399 393 const buttonText = useMemo(() => { 400 - if (loading) { 401 - return "Loading..."; 402 - } 394 + if (loading) return "Loading..."; 403 395 if (!userIsLive) { 404 396 return mode === "create" 405 397 ? "Waiting for stream to start..."
+5 -15
js/app/components/live-dashboard/multistream-status.tsx
··· 47 47 }, [targets, opacity]); 48 48 49 49 const loadTargets = useCallback(async () => { 50 - if (!agent) { 51 - return; 52 - } 50 + if (!agent) return; 53 51 54 52 try { 55 53 setLoading(true); ··· 67 65 68 66 const toggleTarget = useCallback( 69 67 async (target: MultistreamTargetViewHydrated, newActiveState: boolean) => { 70 - if (!agent) { 71 - return; 72 - } 68 + if (!agent) return; 73 69 try { 74 70 setTogglingTargets((prev) => new Set(prev).add(target.uri)); 75 71 await agent.place.stream.multistream.putTarget({ ··· 101 97 const inactiveTargets = targets.filter((t) => !t.record.active); 102 98 103 99 const getTargetName = (target: MultistreamTargetViewHydrated) => { 104 - if (target.record.name) { 105 - return target.record.name; 106 - } 100 + if (target.record.name) return target.record.name; 107 101 if (target.record.url) { 108 102 try { 109 103 const u = new URL(target.record.url); ··· 116 110 }; 117 111 118 112 const getTargetHostname = (target: MultistreamTargetViewHydrated) => { 119 - if (!target.record.url) { 120 - return null; 121 - } 113 + if (!target.record.url) return null; 122 114 try { 123 115 const u = new URL(target.record.url); 124 116 return u.host.split(":")[0]; ··· 128 120 }; 129 121 130 122 const getStatusColor = (target: MultistreamTargetViewHydrated) => { 131 - if (!target.record.active) { 132 - return text.gray[500]; 133 - } 123 + if (!target.record.active) return text.gray[500]; 134 124 135 125 switch (target.latestEvent?.status) { 136 126 case "active":
+2 -6
js/app/components/live-dashboard/stream-monitor.tsx
··· 60 60 61 61 // Connection quality indicator 62 62 const getConnectionIcon = () => { 63 - if (!isLive) { 64 - return null; 65 - } 63 + if (!isLive) return null; 66 64 67 65 switch (segmentTiming.connectionQuality) { 68 66 case "good": ··· 77 75 }; 78 76 79 77 const getConnectionColor = () => { 80 - if (!isLive) { 81 - return "red"; 82 - } 78 + if (!isLive) return "red"; 83 79 84 80 switch (segmentTiming.connectionQuality) { 85 81 case "good":
+1 -3
js/app/components/login/login-form.tsx
··· 69 69 return; 70 70 } 71 71 let clean = handle; 72 - if (handle.startsWith("@")) { 73 - clean = handle.slice(1); 74 - } 72 + if (handle.startsWith("@")) clean = handle.slice(1); 75 73 loginAction(clean, openLoginLink); 76 74 }; 77 75
+1
js/app/components/login/login.tsx
··· 27 27 28 28 // check for stored return route on mount 29 29 useEffect(() => { 30 + if (Platform.OS !== "web") return; 30 31 storage.getItem("returnRoute").then((stored) => { 31 32 if (stored) { 32 33 try {
+1 -3
js/app/components/login/pds-host-selector-modal.tsx
··· 110 110 111 111 const handleSubmit = () => { 112 112 const hostToUse = useCustom ? customHost : selectedHost; 113 - if (!hostToUse) { 114 - return; 115 - } 113 + if (!hostToUse) return; 116 114 117 115 onSubmit(hostToUse); 118 116 handleCancel();
+2 -6
js/app/components/mobile/desktop-ui/kebab.tsx
··· 131 131 return ( 132 132 <DropdownMenuItem 133 133 onPress={() => { 134 - if (!livestream) { 135 - return; 136 - } 134 + if (!livestream) return; 137 135 onOpenChange?.(false); 138 136 setReportModalOpen(true); 139 137 setReportSubject({ ··· 163 161 return ( 164 162 <DropdownMenuItem 165 163 onPress={() => { 166 - if (!profile?.did) { 167 - return; 168 - } 164 + if (!profile?.did) return; 169 165 onOpenChange?.(false); 170 166 setReportModalOpen(true); 171 167 setReportSubject({
+1 -2
js/app/components/mobile/desktop-ui/live-bubble.tsx
··· 15 15 return segDate && Date.now() - segDate.getTime() <= 10 * 1000; 16 16 }, [segDate]); 17 17 18 - if (!isLive) { 18 + if (!isLive) 19 19 return ( 20 20 <View style={[{ flexDirection: "row" }]}> 21 21 <View ··· 43 43 </View> 44 44 </View> 45 45 ); 46 - } 47 46 48 47 return ( 49 48 <View style={[{ flexDirection: "row" }]}>
+1 -3
js/app/components/mobile/desktop-ui/mute-overlay.tsx
··· 15 15 const setMuted = useSetMuted(); 16 16 const setMuteWasForced = usePlayerStore((state) => state.setMuteWasForced); 17 17 18 - if (!muteWasForced) { 19 - return null; 20 - } 18 + if (!muteWasForced) return null; 21 19 22 20 return ( 23 21 <View
+6 -18
js/app/components/mobile/desktop-ui.tsx
··· 90 90 91 91 const resetFadeTimer = useCallback(() => { 92 92 fadeOpacity.value = withTiming(1, { duration: 200 }); 93 - if (fadeTimeout.current) { 94 - clearTimeout(fadeTimeout.current); 95 - } 93 + if (fadeTimeout.current) clearTimeout(fadeTimeout.current); 96 94 setIsControlsVisible(true); 97 95 98 96 fadeTimeout.current = setTimeout(() => { ··· 106 104 }, [resetFadeTimer]); 107 105 108 106 const toggleChat = useCallback(() => { 109 - if (setIsChatOpen) { 110 - setIsChatOpen(!isChatOpen); 111 - } 107 + if (setIsChatOpen) setIsChatOpen(!isChatOpen); 112 108 }, []); 113 109 114 110 useEffect(() => { 115 111 resetFadeTimer(); 116 112 117 113 return () => { 118 - if (fadeTimeout.current) { 119 - clearTimeout(fadeTimeout.current); 120 - } 114 + if (fadeTimeout.current) clearTimeout(fadeTimeout.current); 121 115 if (ingestStarting) { 122 116 setIngestStarting(false); 123 117 } ··· 139 133 140 134 // Picture-in-Picture event listeners 141 135 useEffect(() => { 142 - if (Platform.OS !== "web") { 143 - return; 144 - } 136 + if (Platform.OS !== "web") return; 145 137 146 138 let video: HTMLVideoElement | null = null; 147 139 if (isRefObject(videoRef)) { 148 140 video = videoRef.current; 149 141 } 150 - if (!video) { 151 - return; 152 - } 142 + if (!video) return; 153 143 154 144 function onEnter() { 155 145 setPipActive(true); ··· 170 160 }, [videoRef]); 171 161 172 162 const handlePip = useCallback(() => { 173 - if (pipAction) { 174 - pipAction(); 175 - } 163 + if (pipAction) pipAction(); 176 164 }, [pipAction]); 177 165 178 166 const hover = Gesture.Hover().onChange((_) => runOnJS(onPlayerHover)());
+1 -3
js/app/components/mobile/offline-counter/index.tsx
··· 49 49 return () => clearInterval(interval); 50 50 }, [offline, segment?.startTime]); 51 51 52 - if (!offline) { 53 - return null; 54 - } 52 + if (!offline) return null; 55 53 56 54 const titleFontSize = isMobile ? 24 : 32; 57 55 const subtitleFontSize = isMobile ? 16 : 18;
+3 -9
js/app/components/mobile/ui.tsx
··· 98 98 }, [ingestStarting, setIngestStarting]); 99 99 100 100 useEffect(() => { 101 - if (recordSubmitted) { 102 - setShowLoading(false); 103 - } 101 + if (recordSubmitted) setShowLoading(false); 104 102 }, [recordSubmitted]); 105 103 106 104 const isSelfAndNotLive = ingest === "new"; ··· 118 116 119 117 const resetFadeTimer = () => { 120 118 fadeOpacity.value = withTiming(1, { duration: 200 }); 121 - if (fadeTimeout.current) { 122 - clearTimeout(fadeTimeout.current); 123 - } 119 + if (fadeTimeout.current) clearTimeout(fadeTimeout.current); 124 120 fadeTimeout.current = setTimeout(() => { 125 121 fadeOpacity.value = withTiming(0, { duration: 400 }); 126 122 }, FADE_OUT_DELAY); ··· 129 125 useEffect(() => { 130 126 resetFadeTimer(); 131 127 return () => { 132 - if (fadeTimeout.current) { 133 - clearTimeout(fadeTimeout.current); 134 - } 128 + if (fadeTimeout.current) clearTimeout(fadeTimeout.current); 135 129 }; 136 130 }, []); 137 131
+3 -9
js/app/components/mobile/user-offline.tsx
··· 56 56 const detailedProfile = profile ? pfp[profile?.did] : null; 57 57 58 58 useEffect(() => { 59 - if (!profile?.did) { 60 - return; 61 - } 59 + if (!profile?.did) return; 62 60 63 61 let mounted = true; 64 62 ··· 67 65 try { 68 66 console.log("fetching recommendations for", profile.did); 69 67 const result = await getRecommendations(profile.did); 70 - if (!mounted) { 71 - return; 72 - } 68 + if (!mounted) return; 73 69 if (result.recommendations && result.recommendations.length > 0) { 74 70 // Get the first livestream recommendation 75 71 const firstLivestream = result.recommendations.find( ··· 87 83 } catch (err) { 88 84 console.error("failed to get recommendations", err); 89 85 } finally { 90 - if (mounted) { 91 - setIsLoadingRecommendation(false); 92 - } 86 + if (mounted) setIsLoadingRecommendation(false); 93 87 } 94 88 }; 95 89
+1 -3
js/app/components/settings/key-manager.tsx
··· 98 98 99 99 const [deletingKeys, setDeletingKeys] = useState<Set<string>>(new Set()); 100 100 const deleteKeyRecord = (rkey: string) => { 101 - if (deletingKeys.has(rkey)) { 102 - return; 103 - } // Prevent double deletes 101 + if (deletingKeys.has(rkey)) return; // Prevent double deletes 104 102 setDeletingKeys((prev) => new Set(prev).add(rkey)); 105 103 deleteStreamKeyRecord(rkey).finally(() => { 106 104 setDeletingKeys((prev) => {
+5 -15
js/app/components/settings/multistream-manager.tsx
··· 83 83 const [formError, setFormError] = useState<string>(""); 84 84 85 85 const loadMultistreamTargets = async () => { 86 - if (!agent) { 87 - return; 88 - } 86 + if (!agent) return; 89 87 90 88 try { 91 89 setLoading(true); ··· 104 102 const createMultistreamTarget = async ( 105 103 record: PlaceStreamMultistreamTarget.Record, 106 104 ) => { 107 - if (!agent) { 108 - return; 109 - } 105 + if (!agent) return; 110 106 try { 111 107 setFormError(""); 112 108 setFormLoading(true); ··· 130 126 uri: string, 131 127 record: PlaceStreamMultistreamTarget.Record, 132 128 ) => { 133 - if (!agent) { 134 - return; 135 - } 129 + if (!agent) return; 136 130 try { 137 131 setFormError(""); 138 132 setFormLoading(true); ··· 155 149 target: MultistreamTargetViewHydrated, 156 150 newActiveState: boolean, 157 151 ) => { 158 - if (!agent) { 159 - return; 160 - } 152 + if (!agent) return; 161 153 try { 162 154 setTogglingTargets((prev) => new Set(prev).add(target.uri)); 163 155 await agent.place.stream.multistream.putTarget({ ··· 181 173 }; 182 174 183 175 const deleteMultistreamTarget = async (uri: string) => { 184 - if (!agent) { 185 - return; 186 - } 176 + if (!agent) return; 187 177 try { 188 178 setFormError(""); 189 179 setDeletingTargets((prev) => new Set(prev).add(uri));
+5 -15
js/app/components/settings/recommendations-manager.tsx
··· 63 63 const { t } = useTranslation("settings"); 64 64 65 65 const loadRecommendations = async () => { 66 - if (!agent) { 67 - return; 68 - } 66 + if (!agent) return; 69 67 70 68 try { 71 69 setLoading(true); ··· 113 111 }; 114 112 115 113 const saveRecommendations = async (newStreamers: string[]) => { 116 - if (!agent || saving) { 117 - return; 118 - } 114 + if (!agent || saving) return; 119 115 120 116 try { 121 117 if (!agent.did) { ··· 253 249 }; 254 250 255 251 const handleSaveEdit = async () => { 256 - if (editingIndex === null) { 257 - return; 258 - } 252 + if (editingIndex === null) return; 259 253 260 254 const trimmed = editValue.trim(); 261 255 if (!trimmed) { ··· 291 285 }; 292 286 293 287 const confirmDelete = async () => { 294 - if (deleteDialog.index === null) { 295 - return; 296 - } 288 + if (deleteDialog.index === null) return; 297 289 298 290 const newStreamers = streamers.filter((_, i) => i !== deleteDialog.index); 299 291 await saveRecommendations(newStreamers); ··· 301 293 }; 302 294 303 295 useEffect(() => { 304 - if (!agent) { 305 - return; 306 - } 296 + if (!agent) return; 307 297 loadRecommendations(); 308 298 }, [agent]); 309 299
+6 -18
js/app/components/settings/webhook-manager.tsx
··· 631 631 const { t } = useTranslation("settings"); 632 632 633 633 const loadWebhooks = async () => { 634 - if (!agent) { 635 - return; 636 - } 634 + if (!agent) return; 637 635 638 636 try { 639 637 setLoading(true); ··· 659 657 }; 660 658 661 659 const createWebhook = async (data: WebhookFormData) => { 662 - if (!agent) { 663 - return; 664 - } 660 + if (!agent) return; 665 661 666 662 try { 667 663 setFormLoading(true); ··· 697 693 }; 698 694 699 695 const updateWebhook = async (data: WebhookFormData) => { 700 - if (!agent || !editingWebhook) { 701 - return; 702 - } 696 + if (!agent || !editingWebhook) return; 703 697 704 698 try { 705 699 setFormLoading(true); ··· 737 731 738 732 const deleteWebhook = async (id: string) => { 739 733 const webhook = webhooks?.find((w) => w.id === id); 740 - if (!webhook) { 741 - return; 742 - } 734 + if (!webhook) return; 743 735 744 736 setDeleteDialog({ isVisible: true, webhook }); 745 737 }; 746 738 747 739 const confirmDelete = async () => { 748 - if (!agent || !deleteDialog.webhook) { 749 - return; 750 - } 740 + if (!agent || !deleteDialog.webhook) return; 751 741 752 742 const id = deleteDialog.webhook.id; 753 743 ··· 790 780 }; 791 781 792 782 useEffect(() => { 793 - if (!agent) { 794 - return; 795 - } 783 + if (!agent) return; 796 784 loadWebhooks(); 797 785 }, [agent]); 798 786
+6 -1
js/app/features/bluesky/blueskyProvider.tsx
··· 2 2 import { storage } from "@streamplace/components"; 3 3 import { useURL } from "expo-linking"; 4 4 import { useEffect, useState } from "react"; 5 + import { Platform } from "react-native"; 5 6 import { useStore } from "store"; 6 7 import { useIsReady, useOAuthSession, useUserProfile } from "store/hooks"; 7 8 import { navigateToRoute } from "utils/navigation"; ··· 23 24 loadOAuthClient(); 24 25 25 26 // load return route from storage on mount 27 + if (Platform.OS !== "web") { 28 + return; 29 + } 26 30 storage.getItem("returnRoute").then((stored) => { 27 31 if (stored) { 28 32 try { ··· 82 86 if ( 83 87 lastAuthStatus !== "loggedIn" && 84 88 authStatus === "loggedIn" && 85 - returnRoute 89 + returnRoute && 90 + Platform.OS === "web" 86 91 ) { 87 92 console.log( 88 93 "Login successful, navigating back to returnRoute:",
+2 -6
js/app/hooks/usePreloadEmoji.ts
··· 6 6 7 7 export function usePreloadEmoji({ immediate }: { immediate?: boolean } = {}) { 8 8 const preload = React.useCallback(async () => { 9 - if (loadRequested) { 10 - return; 11 - } 9 + if (loadRequested) return; 12 10 loadRequested = true; 13 11 let data; 14 12 if (Platform.OS === "web") { ··· 19 17 init({ data }); 20 18 }, []); 21 19 22 - if (immediate) { 23 - preload(); 24 - } 20 + if (immediate) preload(); 25 21 return preload; 26 22 }
+1 -3
js/app/hooks/useSegmentTiming.tsx
··· 8 8 range: number | null, 9 9 numOfSegments: number = 1, 10 10 ): ConnectionQuality { 11 - if (timeBetweenSegments === null || range === null) { 12 - return "poor"; 13 - } 11 + if (timeBetweenSegments === null || range === null) return "poor"; 14 12 15 13 if (timeBetweenSegments <= 1500 && range <= (1500 * 60) / numOfSegments) { 16 14 return "good";
+1 -3
js/app/hooks/useSidebarControl.tsx
··· 49 49 const isActive = useIsLargeScreen(); 50 50 useEffect(() => { 51 51 if (isActive) { 52 - if (!isHidden && targetWidth < 64) { 53 - targetWidth == 64; 54 - } 52 + if (!isHidden && targetWidth < 64) targetWidth == 64; 55 53 // Only animate if the sidebar is active 56 54 animatedWidth.value = withTiming(targetWidth, { duration: 250 }); 57 55 } else {
+1 -3
js/app/src/screens/chat-popout.native.tsx
··· 65 65 }, [seg?.id]); 66 66 67 67 const getLatency = useCallback((): string => { 68 - if (!segmentReceivedTimeRef.current) { 69 - return ""; 70 - } 68 + if (!segmentReceivedTimeRef.current) return ""; 71 69 const secondsAgo = Math.floor(Date.now() - segmentReceivedTimeRef.current); 72 70 const isThreeDigits = secondsAgo >= 100 && secondsAgo < 1000; 73 71 if (isThreeDigits) {
+5 -15
js/app/src/screens/danmu-obs.tsx
··· 22 22 const params: DanmuParams = {}; 23 23 24 24 const opacity = query.get("opacity"); 25 - if (opacity) { 26 - params.opacity = parseInt(opacity); 27 - } 25 + if (opacity) params.opacity = parseInt(opacity); 28 26 29 27 const speed = query.get("speed"); 30 - if (speed) { 31 - params.speed = parseFloat(speed); 32 - } 28 + if (speed) params.speed = parseFloat(speed); 33 29 34 30 const laneCount = query.get("laneCount"); 35 - if (laneCount) { 36 - params.laneCount = parseInt(laneCount); 37 - } 31 + if (laneCount) params.laneCount = parseInt(laneCount); 38 32 39 33 const maxMessages = query.get("maxMessages"); 40 - if (maxMessages) { 41 - params.maxMessages = parseInt(maxMessages); 42 - } 34 + if (maxMessages) params.maxMessages = parseInt(maxMessages); 43 35 44 36 const enabled = query.get("enabled"); 45 - if (enabled !== null) { 46 - params.enabled = enabled !== "false"; 47 - } 37 + if (enabled !== null) params.enabled = enabled !== "false"; 48 38 49 39 return params; 50 40 };
+9 -27
js/app/src/screens/home.tsx
··· 64 64 } 65 65 66 66 function getHomeScreenItemSize(width: number): StreamCardSize { 67 - if (width >= 1536) { 68 - return "md"; 69 - } // xxl 70 - if (width >= 1280) { 71 - return "sm"; 72 - } // xl 73 - if (width >= 1024) { 74 - return "sm"; 75 - } // lg 76 - if (width >= 768) { 77 - return "sm"; 78 - } // md 67 + if (width >= 1536) return "md"; // xxl 68 + if (width >= 1280) return "sm"; // xl 69 + if (width >= 1024) return "sm"; // lg 70 + if (width >= 768) return "sm"; // md 79 71 return "xs"; // sm and below 80 72 } 81 73 82 74 function getHomeScreenCols(width: number): number { 83 - if (width >= 1550) { 84 - return 4; 85 - } 86 - if (width >= 1280) { 87 - return 3; 88 - } 89 - if (width >= 1024) { 90 - return 2; 91 - } 92 - if (width >= 768) { 93 - return 2; 94 - } 75 + if (width >= 1550) return 4; 76 + if (width >= 1280) return 3; 77 + if (width >= 1024) return 2; 78 + if (width >= 768) return 2; 95 79 return 1; 96 80 } 97 81 98 82 // Get the ratio for the first card based on column count 99 83 function getPadPercentage(cols: number): number { 100 - if (cols >= 3) { 101 - return 2.07; 102 - } 84 + if (cols >= 3) return 2.07; 103 85 return 1; 104 86 } 105 87
+1 -3
js/app/store/hooks.ts
··· 25 25 const oauthSession = useOAuthSession(); 26 26 const profiles = useProfiles(); 27 27 const did = oauthSession?.did; 28 - if (!did) { 29 - return null; 30 - } 28 + if (!did) return null; 31 29 return profiles[did]; 32 30 }; 33 31 export const useIsReady = () => {
+2 -6
js/app/store/slices/platformSlice.native.ts
··· 38 38 notificationToken: null, 39 39 notificationDestination: null, 40 40 handleNotification: (payload) => { 41 - if (!payload) { 42 - return; 43 - } 44 - if (typeof payload.path !== "string") { 45 - return; 46 - } 41 + if (!payload) return; 42 + if (typeof payload.path !== "string") return; 47 43 set({ notificationDestination: payload.path }); 48 44 }, 49 45 clearNotification: () => {
+1 -3
js/atproto-oauth-client-react-native/src/sqlite-keystore.ts
··· 22 22 23 23 async get(key: string): Promise<T | undefined> { 24 24 const itemStr = await this.store.get(key); 25 - if (!itemStr) { 26 - return undefined; 27 - } 25 + if (!itemStr) return undefined; 28 26 const item = JSON.parse(itemStr) as T; 29 27 if (item.dpopKey) { 30 28 item.dpopKey = new JoseKey(item.dpopKey as unknown as Jwk);
+1 -3
js/components/scripts/migrate-i18n.js
··· 353 353 allNewKeys.push(...namespacesForLocale[namespace]); 354 354 } 355 355 356 - if (allNewKeys.length === 0) { 357 - continue; 358 - } 356 + if (allNewKeys.length === 0) continue; 359 357 360 358 // Add all keys to the specified namespace 361 359 const targetFile = addKeysToFtlFile(
+8 -24
js/components/src/components/chat/chat-box.tsx
··· 93 93 }, [pdsAgent, userDID, setActiveTeleportUri]); 94 94 95 95 const authors = useMemo(() => { 96 - if (!chat) { 97 - return null; 98 - } 96 + if (!chat) return null; 99 97 return chat.reduce((acc, msg) => { 100 98 // our fake system user "did" 101 - if (msg.author.did === "did:sys:system") { 102 - return acc; 103 - } 104 - if (acc.has(msg.author.handle)) { 105 - return acc; 106 - } 99 + if (msg.author.did === "did:sys:system") return acc; 100 + if (acc.has(msg.author.handle)) return acc; 107 101 acc.set(msg.author.handle, msg.chatProfile); 108 102 return acc; 109 103 }, new Map<string, ChatMessageViewHydrated["chatProfile"]>()); ··· 149 143 if (colonIndex !== -1) { 150 144 const searchText = text.slice(colonIndex + 1).toLowerCase(); 151 145 if (searchText.length >= 3) { 152 - if (!emojiData) { 153 - return; 154 - } 146 + if (!emojiData) return; 155 147 const aliasMatches = Object.entries(emojiData.aliases) 156 148 .map(([alias, emojiId]) => { 157 149 const aliasLower = alias.toLowerCase(); ··· 177 169 { matchType: number; index: number; alias: string } 178 170 > = {}; 179 171 for (const match of aliasMatches) { 180 - if (!match) { 181 - continue; 182 - } 172 + if (!match) continue; 183 173 const prev = bestAliasMatch[match.emojiId]; 184 174 if ( 185 175 !prev || ··· 242 232 // Sort by alias match type, then position, then fallback 243 233 .sort((a, b) => { 244 234 for (let i = 0; i < a.sort.length; ++i) { 245 - if (a.sort[i] !== b.sort[i]) { 246 - return a.sort[i] - b.sort[i]; 247 - } 235 + if (a.sort[i] !== b.sort[i]) return a.sort[i] - b.sort[i]; 248 236 } 249 237 return 0; 250 238 }) ··· 270 258 }; 271 259 272 260 const submit = async () => { 273 - if (!message.trim()) { 274 - return; 275 - } 261 + if (!message.trim()) return; 276 262 if (graphemer.countGraphemes(message) > 300) { 277 263 toast.show( 278 264 "Message too long", ··· 589 575 aria-label="Popout Chat" 590 576 style={{ borderRadius: 16, maxWidth: 44, aspectRatio: 1 }} 591 577 onPress={() => { 592 - if (!linfo) { 593 - return; 594 - } 578 + if (!linfo) return; 595 579 const u = new URL(window.location.href); 596 580 u.pathname = `/chat-popout/${linfo?.author?.did}`; 597 581 window.open(u.toString(), "_blank", "popup=true");
+1 -3
js/components/src/components/chat/chat-message.tsx
··· 83 83 text: string; 84 84 facets: ChatMessageViewHydrated["record"]["facets"]; 85 85 }) => { 86 - if (!facets?.length) { 87 - return <Text>{text}</Text>; 88 - } 86 + if (!facets?.length) return <Text>{text}</Text>; 89 87 90 88 const userCache = useLivestreamStore((state) => state.authors); 91 89
+2 -5
js/components/src/components/chat/chat.tsx
··· 77 77 const setReply = useSetReplyToMessage(); 78 78 const setModMsg = usePlayerStore((state) => state.setModMessage); 79 79 80 - if (!visible) { 81 - return null; 82 - } 80 + if (!visible) return null; 83 81 84 82 return ( 85 83 <View ··· 292 290 } 293 291 }; 294 292 295 - if (!chat) { 293 + if (!chat) 296 294 return ( 297 295 <View style={[flex.shrink[1], { minWidth: 0, maxWidth: "100%" }]}> 298 296 <Text>Loading chat...</Text> 299 297 </View> 300 298 ); 301 - } 302 299 303 300 return ( 304 301 <View
+4 -12
js/components/src/components/chat/mod-view.tsx
··· 206 206 <DropdownMenuItem 207 207 disabled={isHideLoading || messageRemoved} 208 208 onPress={() => { 209 - if (isHideLoading || messageRemoved) { 210 - return; 211 - } 209 + if (isHideLoading || messageRemoved) return; 212 210 createHideChat(message.uri, streamerDID ?? undefined) 213 211 .then((r) => setMessageRemoved(true)) 214 212 .catch((e) => console.error(e)); ··· 232 230 <DropdownMenuItem 233 231 disabled={isBlockLoading} 234 232 onPress={() => { 235 - if (isBlockLoading) { 236 - return; 237 - } 233 + if (isBlockLoading) return; 238 234 createBlock(message.author.did, streamerDID ?? undefined) 239 235 .then((r) => { 240 236 toast.show( ··· 315 311 <DropdownMenuItem 316 312 closeOnPress={false} 317 313 onPress={() => { 318 - if (!message) { 319 - return; 320 - } 314 + if (!message) return; 321 315 if (!confirming) { 322 316 setConfirming(DeleteState.Confirmed); 323 317 return; ··· 362 356 return ( 363 357 <DropdownMenuItem 364 358 onPress={() => { 365 - if (!message) { 366 - return; 367 - } 359 + if (!message) return; 368 360 onOpenChange?.(false); 369 361 setReportModalOpen(true); 370 362 setReportSubject({
+1 -3
js/components/src/components/content-metadata/content-metadata-form.tsx
··· 137 137 } 138 138 139 139 const loadMetadata = async () => { 140 - if (!pdsAgent || !did) { 141 - return; 142 - } 140 + if (!pdsAgent || !did) return; 143 141 144 142 try { 145 143 const metadata = await getContentMetadata();
+2 -5
js/components/src/components/danmu/danmu-message.tsx
··· 71 71 }; 72 72 73 73 useEffect(() => { 74 - if (messageWidth === 0) { 75 - return; 76 - } // Wait for layout measurement 74 + if (messageWidth === 0) return; // Wait for layout measurement 77 75 78 76 const duration = baseDuration(message, MAX_DURATION, MIN_DURATION); 79 77 ··· 92 90 setTotalDuration(duration); 93 91 } 94 92 95 - if (__DEV__) { 93 + if (__DEV__) 96 94 console.log( 97 95 `[danmu] animation started: "${message.record.text}" (duration: ${duration.toFixed(0)}ms, remaining: ${remainingDuration.toFixed(0)}ms, speed: ${speed}x)`, 98 96 ); 99 - } 100 97 101 98 translateX.value = startPosition; 102 99
+4 -11
js/components/src/components/danmu/danmu-overlay.tsx
··· 81 81 ); 82 82 83 83 useEffect(() => { 84 - if (!enabled || !chat || containerWidth === 0) { 85 - return; 86 - } 84 + if (!enabled || !chat || containerWidth === 0) return; 87 85 88 86 // only check new messages since last render (chat is sorted newest first) 89 87 const newMessageCount = chat.length - lastChatLength.current; 90 - if (newMessageCount <= 0) { 91 - return; 92 - } 88 + if (newMessageCount <= 0) return; 93 89 94 90 const messagesToCheck = chat.slice(0, newMessageCount); 95 91 lastChatLength.current = chat.length; ··· 103 99 return !hasProcessed && !isSystem && isAfterMount; 104 100 }); 105 101 106 - if (newMessages.length === 0) { 107 - return; 108 - } 102 + if (newMessages.length === 0) return; 109 103 110 104 const messagesToAdd: ActiveDanmuMessage[] = []; 111 105 ··· 125 119 } 126 120 127 121 const duration = baseDuration(message, MAX_DURATION, MIN_DURATION); 128 - if (__DEV__) { 122 + if (__DEV__) 129 123 console.log("[danmu] message", message.record.text, { 130 124 duration, 131 125 speed, 132 126 }); 133 - } 134 127 const lane = assignLane(message.uri, duration); 135 128 136 129 if (lane !== null) {
+1 -3
js/components/src/components/danmu/use-danmu-lanes.ts
··· 34 34 (d) => d.lane === laneIndex && d.endTime > now, 35 35 ); 36 36 37 - if (danmuInLane.length === 0) { 38 - return true; 39 - } 37 + if (danmuInLane.length === 0) return true; 40 38 41 39 // check the most recent danmu in this lane 42 40 const mostRecent = danmuInLane.reduce((latest, current) =>
+4 -12
js/components/src/components/dashboard/header.tsx
··· 42 42 43 43 function StatusIndicator({ status, isLive }: StatusIndicatorProps) { 44 44 const getStatusColor = () => { 45 - if (!isLive) { 46 - return bg.gray[500]; 47 - } 45 + if (!isLive) return bg.gray[500]; 48 46 switch (status) { 49 47 case "excellent": 50 48 return bg.green[500]; ··· 60 58 }; 61 59 62 60 const getStatusText = () => { 63 - if (!isLive) { 64 - return "OFFLINE"; 65 - } 61 + if (!isLive) return "OFFLINE"; 66 62 switch (status) { 67 63 case "excellent": 68 64 return "EXCELLENT"; ··· 121 117 onProblemsPress, 122 118 }: HeaderProps) { 123 119 const getConnectionQuality = (): "good" | "warning" | "error" => { 124 - if (timeBetweenSegments <= 1500) { 125 - return "good"; 126 - } 127 - if (timeBetweenSegments <= 3000) { 128 - return "warning"; 129 - } 120 + if (timeBetweenSegments <= 1500) return "good"; 121 + if (timeBetweenSegments <= 3000) return "warning"; 130 122 return "error"; 131 123 }; 132 124
+11 -33
js/components/src/components/dashboard/information-widget.tsx
··· 54 54 const viewers = useViewers(); 55 55 56 56 const getBitrate = useCallback((): number => { 57 - if (!seg?.size || !seg?.duration) { 58 - return 0; 59 - } 57 + if (!seg?.size || !seg?.duration) return 0; 60 58 const kbps = 61 59 (seg.size * 8) / ((seg.duration || 1000000000) / 1000000000) / 1000; 62 60 return kbps; ··· 94 92 }, [seg?.startTime, streamStartTime]); 95 93 96 94 const getBitrateStatus = (): "good" | "warning" | "error" | "neutral" => { 97 - if (currentBitrate > 2000) { 98 - return "good"; 99 - } 100 - if (currentBitrate > 1000) { 101 - return "warning"; 102 - } 103 - if (currentBitrate > 0) { 104 - return "error"; 105 - } 95 + if (currentBitrate > 2000) return "good"; 96 + if (currentBitrate > 1000) return "warning"; 97 + if (currentBitrate > 0) return "error"; 106 98 return "neutral"; 107 99 }; 108 100 109 101 const getConnectionStatus = (): "good" | "warning" | "error" | "neutral" => { 110 - if (!seg) { 111 - return "error"; 112 - } 113 - if (currentBitrate > 1500) { 114 - return "good"; 115 - } 116 - if (currentBitrate > 500) { 117 - return "warning"; 118 - } 102 + if (!seg) return "error"; 103 + if (currentBitrate > 1500) return "good"; 104 + if (currentBitrate > 500) return "warning"; 119 105 return "error"; 120 106 }; 121 107 ··· 454 440 const maxDataValue = Math.max(...data, 1); 455 441 const minDataValue = Math.min(...data); 456 442 const getSmartRange = (max: number) => { 457 - if (max <= 1000) { 458 - return { min: 0, max: 1000, step: 500 }; 459 - } 460 - if (max <= 2000) { 461 - return { min: 1000, max: 2000, step: 1000 }; 462 - } 463 - if (max <= 7000) { 464 - return { min: 4000, max: 7000, step: 1500 }; 465 - } 466 - if (max <= 10000) { 467 - return { min: 4000, max: 10000, step: 5000 }; 468 - } 443 + if (max <= 1000) return { min: 0, max: 1000, step: 500 }; 444 + if (max <= 2000) return { min: 1000, max: 2000, step: 1000 }; 445 + if (max <= 7000) return { min: 4000, max: 7000, step: 1500 }; 446 + if (max <= 10000) return { min: 4000, max: 10000, step: 5000 }; 469 447 470 448 const roundedMax = Math.ceil(max / 5000) * 5000; 471 449 return { min: 0, max: roundedMax, step: roundedMax / 2 };
+2 -6
js/components/src/components/dashboard/moderator-panel.tsx
··· 388 388 // Clear error when user starts typing 389 389 const handleDIDChange = (text: string) => { 390 390 setModeratorDID(text); 391 - if (error) { 392 - setError(null); 393 - } 391 + if (error) setError(null); 394 392 }; 395 393 396 394 const handleAdd = async () => { ··· 430 428 <ResponsiveDialog 431 429 open={visible} 432 430 onOpenChange={(open) => { 433 - if (!open) { 434 - onClose(); 435 - } 431 + if (!open) onClose(); 436 432 }} 437 433 title="Add Moderator" 438 434 description="Enter the DID or handle of the user you want to add as a moderator and select their permissions."
+6 -18
js/components/src/components/mobile-player/rotation-lock.tsx
··· 59 59 60 60 // Manual rotation functions 61 61 const rotateToLandscape = async () => { 62 - if (!enabled || !canRotate || !ScreenOrientation) { 63 - return; 64 - } 62 + if (!enabled || !canRotate || !ScreenOrientation) return; 65 63 66 64 try { 67 65 await ScreenOrientation.unlockAsync(); ··· 82 80 }; 83 81 84 82 const rotateToPortrait = async () => { 85 - if (!enabled || !canRotate || !ScreenOrientation) { 86 - return; 87 - } 83 + if (!enabled || !canRotate || !ScreenOrientation) return; 88 84 89 85 try { 90 86 await ScreenOrientation.unlockAsync(); ··· 104 100 }; 105 101 106 102 const toggleRotation = async () => { 107 - if (!ScreenOrientation) { 108 - return; 109 - } 103 + if (!ScreenOrientation) return; 110 104 111 105 const isLandscape = 112 106 currentOrientation === ScreenOrientation.Orientation.LANDSCAPE_LEFT || ··· 127 121 128 122 // Track orientation changes 129 123 useEffect(() => { 130 - if (!enabled) { 131 - return; 132 - } 124 + if (!enabled) return; 133 125 134 126 const getCurrentOrientation = async () => { 135 - if (!ScreenOrientation) { 136 - return; 137 - } 127 + if (!ScreenOrientation) return; 138 128 try { 139 129 const orient = await ScreenOrientation.getOrientationAsync(); 140 130 setCurrentOrientation(orient); ··· 149 139 150 140 getCurrentOrientation(); 151 141 152 - if (!ScreenOrientation) { 153 - return; 154 - } 142 + if (!ScreenOrientation) return; 155 143 156 144 const subscription = ScreenOrientation.addOrientationChangeListener( 157 145 (event) => {
+1 -3
js/components/src/components/mobile-player/ui/autoplay-button.tsx
··· 40 40 } 41 41 }; 42 42 43 - if (!autoplayFailed) { 44 - return null; 45 - } 43 + if (!autoplayFailed) return null; 46 44 47 45 return ( 48 46 <View
+3 -9
js/components/src/components/mobile-player/ui/countdown.tsx
··· 36 36 }; 37 37 38 38 const handleDone = () => { 39 - if (onDone) { 40 - onDone(); 41 - } 39 + if (onDone) onDone(); 42 40 }; 43 41 44 42 // Accurate countdown using useFrameCallback 45 43 useFrameCallback(({ timestamp }) => { 46 - if (!visible) { 47 - return; 48 - } 44 + if (!visible) return; 49 45 50 46 // Set start timestamp on first frame 51 47 if (startTimestamp.value === null) { ··· 90 86 opacity: opacity.value, 91 87 })); 92 88 93 - if (!visible || countdown === 0) { 94 - return null; 95 - } 89 + if (!visible || countdown === 0) return null; 96 90 97 91 return ( 98 92 <Animated.View
+1 -3
js/components/src/components/mobile-player/ui/report-modal.tsx
··· 84 84 }; 85 85 86 86 const handleSubmit = async () => { 87 - if (!selectedReason) { 88 - return; 89 - } 87 + if (!selectedReason) return; 90 88 91 89 setIsSubmitting(true); 92 90 setSubmitError(null);
+2 -6
js/components/src/components/mobile-player/ui/streamer-loading-overlay.tsx
··· 80 80 81 81 // Trigger animation on each message change 82 82 useEffect(() => { 83 - if (!visible) { 84 - return; 85 - } 83 + if (!visible) return; 86 84 87 85 const fadeDuration = Math.min(interval / 2, 250); // Simplified fade duration 88 86 ··· 117 115 opacity: wholeOpacity.value, 118 116 })); 119 117 120 - if (!shouldRender) { 121 - return null; 122 - } 118 + if (!shouldRender) return null; 123 119 124 120 return ( 125 121 <Animated.View
+31 -5
js/components/src/components/mobile-player/ui/viewer-context-menu.tsx
··· 58 58 const setReportModalOpen = usePlayerStore((x) => x.setReportModalOpen); 59 59 const setReportSubject = usePlayerStore((x) => x.setReportSubject); 60 60 61 + const latestSegment = useLivestreamStore((x) => x.segment); 62 + // get highest height x width rendition for video 63 + const videoRendition = latestSegment?.video?.reduce((prev, current) => { 64 + const prevPixels = prev.width * prev.height; 65 + const currentPixels = current.width * current.height; 66 + return currentPixels > prevPixels ? current : prev; 67 + }, latestSegment?.video?.[0]); 68 + const highestLength = videoRendition 69 + ? videoRendition.height < videoRendition.width 70 + ? videoRendition.height 71 + : videoRendition?.width 72 + : 0; 73 + 74 + // ugh i hate this 75 + const frames = videoRendition?.framerate as 76 + | { num: number; den: number } 77 + | undefined; 78 + const fps = 79 + frames?.num && frames?.den 80 + ? Math.round((frames.num / frames.den) * 100) / 100 81 + : 0; 82 + 83 + const resolutionDisplay = highestLength 84 + ? `(${highestLength}p${fps > 0 ? fps : ""})` 85 + : "(Original Quality)"; 86 + 61 87 const { profile } = useLivestreamInfo(); 62 88 63 89 const avatars = useAvatars(profile?.did ? [profile?.did] : []); ··· 215 241 > 216 242 <Text>Quality</Text> 217 243 <Text muted size={isMobile ? "base" : "sm"}> 218 - {quality === "source" ? "Source" : quality},{" "} 244 + {quality === "source" 245 + ? `Source${resolutionDisplay ? " " + resolutionDisplay + "\n" : ", "}` 246 + : quality} 219 247 {lowLatency ? "Low Latency" : ""} 220 248 </Text> 221 249 </View> ··· 227 255 onValueChange={setQuality} 228 256 > 229 257 <DropdownMenuRadioItem value="source"> 230 - <Text>Source (Original Quality)</Text> 258 + <Text>Source {resolutionDisplay}</Text> 231 259 </DropdownMenuRadioItem> 232 260 {qualities.map((r) => ( 233 261 <DropdownMenuRadioItem key={r.name} value={r.name}> ··· 294 322 return ( 295 323 <DropdownMenuItem 296 324 onPress={() => { 297 - if (!livestream) { 298 - return; 299 - } 325 + if (!livestream) return; 300 326 onOpenChange?.(false); 301 327 setReportModalOpen(true); 302 328 setReportSubject({
+5 -14
js/components/src/components/mobile-player/video.tsx
··· 29 29 | undefined, 30 30 instance: HTMLVideoElement | null, 31 31 ) { 32 - if (!ref) { 33 - return; 34 - } 35 - if (typeof ref === "function") { 36 - ref(instance); 37 - } else { 38 - ref.current = instance; 39 - } 32 + if (!ref) return; 33 + if (typeof ref === "function") ref(instance); 34 + else ref.current = instance; 40 35 } 41 36 42 37 type VideoProps = { ··· 50 45 const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); 51 46 52 47 useEffect(() => { 53 - if (!videoRef.current) { 54 - return; 55 - } 48 + if (!videoRef.current) return; 56 49 57 50 function updateSize() { 58 51 setDimensions({ ··· 393 386 } 394 387 }, [diagnostics]); 395 388 396 - if (!diagnostics.done) { 397 - return <></>; 398 - } 389 + if (!diagnostics.done) return <></>; 399 390 400 391 if (webrtcError) { 401 392 setProtocol(PlayerProtocol.HLS);
+1 -3
js/components/src/components/stream-notification/teleport-notification.tsx
··· 89 89 }, []); 90 90 91 91 useEffect(() => { 92 - if (showStripes) { 93 - return; 94 - } 92 + if (showStripes) return; 95 93 96 94 // animate progress bar 97 95 const percentage = (timeLeft / countdown) * 100;
+1 -3
js/components/src/components/ui/admonition.tsx
··· 26 26 const { theme, icons } = useTheme(); 27 27 28 28 const defaultIconLeft = (() => { 29 - if (iconLeft) { 30 - return iconLeft; 31 - } 29 + if (iconLeft) return iconLeft; 32 30 switch (variant) { 33 31 case "success": 34 32 return CheckCircle;
+3 -9
js/components/src/components/ui/dialog.tsx
··· 350 350 const { theme } = useTheme(); 351 351 const styles = React.useMemo(() => createStyles(theme), [theme]); 352 352 353 - if (!children) { 354 - return null; 355 - } 353 + if (!children) return null; 356 354 357 355 return ( 358 356 <Text ref={ref} style={[styles.title, style]} {...props}> ··· 375 373 const { theme } = useTheme(); 376 374 const styles = React.useMemo(() => createStyles(theme), [theme]); 377 375 378 - if (!children) { 379 - return null; 380 - } 376 + if (!children) return null; 381 377 382 378 return ( 383 379 <Text ref={ref} style={[styles.description, style]} {...props}> ··· 418 414 const { theme } = useTheme(); 419 415 const styles = React.useMemo(() => createStyles(theme), [theme]); 420 416 421 - if (!children) { 422 - return null; 423 - } 417 + if (!children) return null; 424 418 425 419 return ( 426 420 <ModalPrimitive.Footer
+3 -8
js/components/src/components/ui/dropdown.native.tsx
··· 190 190 191 191 const push = (item: NavigationStackItem) => { 192 192 setStack((prev) => { 193 - if (!Array.isArray(prev)) { 193 + if (!Array.isArray(prev)) 194 194 return [{ key: "root", content: children }, item]; 195 - } 196 195 return [...prev, item]; 197 196 }); 198 197 ··· 221 220 }; 222 221 223 222 const pop = () => { 224 - if (stack.length <= 1) { 225 - return; 226 - } 223 + if (stack.length <= 1) return; 227 224 228 225 slideAnim.value = withTiming(40, { duration: 150 }); 229 226 fadeAnim.value = withTiming(0, { duration: 150 }, (finished) => { ··· 247 244 const isNested = stack.length > 1; 248 245 249 246 const onBackgroundTap = () => { 250 - if (sheetRef.current) { 251 - sheetRef.current?.close(); 252 - } 247 + if (sheetRef.current) sheetRef.current?.close(); 253 248 254 249 setTimeout(() => { 255 250 onOpenChange?.(false);
+1 -3
js/components/src/components/ui/primitives/button.tsx
··· 198 198 199 199 export const ButtonLoading = forwardRef<View, ButtonLoadingProps>( 200 200 ({ children, visible = false, style, ...props }, ref) => { 201 - if (!visible) { 202 - return null; 203 - } 201 + if (!visible) return null; 204 202 205 203 return ( 206 204 <View ref={ref} style={[primitiveStyles.loading, style]} {...props}>
+1 -3
js/components/src/components/ui/primitives/input.tsx
··· 230 230 231 231 export const InputError = forwardRef<Text, InputErrorProps>( 232 232 ({ children, visible = true, style, ...props }, ref) => { 233 - if (!visible || !children) { 234 - return null; 235 - } 233 + if (!visible || !children) return null; 236 234 237 235 return ( 238 236 <Text ref={ref} style={[primitiveStyles.error, style]} {...props}>
+2 -6
js/components/src/components/ui/resizeable.tsx
··· 67 67 }) 68 68 .onUpdate((event) => { 69 69 let newHeight = startHeight.value - event.translationY; 70 - if (newHeight > MAX_HEIGHT) { 71 - newHeight = MAX_HEIGHT; 72 - } 73 - if (newHeight < MIN_HEIGHT) { 74 - newHeight = MIN_HEIGHT; 75 - } 70 + if (newHeight > MAX_HEIGHT) newHeight = MAX_HEIGHT; 71 + if (newHeight < MIN_HEIGHT) newHeight = MIN_HEIGHT; 76 72 sheetHeight.value = newHeight; 77 73 78 74 const nowCollapsed = newHeight < COLLAPSE_HEIGHT;
+1 -3
js/components/src/components/ui/toast.tsx
··· 400 400 key={toastState.id} 401 401 {...toastState} 402 402 onOpenChange={(open) => { 403 - if (!open) { 404 - toastManager.hide(toastState.id); 405 - } 403 + if (!open) toastManager.hide(toastState.id); 406 404 }} 407 405 index={index} 408 406 isLatest={index === 0}
+1 -3
js/components/src/hooks/useAvatars.tsx
··· 18 18 ); 19 19 20 20 useEffect(() => { 21 - if (missingDids.length === 0 || !agent) { 22 - return; 23 - } 21 + if (missingDids.length === 0 || !agent) return; 24 22 const toFetch = missingDids.slice(0, 25); 25 23 toFetch.forEach((did) => inFlight.current.add(did)); 26 24
+1 -3
js/components/src/hooks/useLivestreamInfo.ts
··· 41 41 ) => { 42 42 if (!ingestStarting) { 43 43 // Optionally close keyboard if provided 44 - if (closeKeyboard) { 45 - closeKeyboard(); 46 - } 44 + if (closeKeyboard) closeKeyboard(); 47 45 setShowCountdown(true); 48 46 setIngestStarting(true); 49 47 setIngestLive(true);
+1 -3
js/components/src/hooks/useSegmentTiming.tsx
··· 8 8 range: number | null, 9 9 numOfSegments: number = 1, 10 10 ): ConnectionQuality { 11 - if (timeBetweenSegments === null || range === null) { 12 - return "poor"; 13 - } 11 + if (timeBetweenSegments === null || range === null) return "poor"; 14 12 15 13 if (timeBetweenSegments <= 1500 && range <= (1500 * 60) / numOfSegments) { 16 14 return "good";
+1 -3
js/components/src/i18n/i18next-config.ts
··· 94 94 function getFallbackChain(code: string): string[] { 95 95 const fallbacks: string[] = []; 96 96 97 - if (!code) { 98 - return manifest.fallbackChain; 99 - } 97 + if (!code) return manifest.fallbackChain; 100 98 101 99 // Regional fallbacks 102 100 if (code.match(/^es-/)) {
+1 -3
js/components/src/lib/system-messages.ts
··· 162 162 const type = getSystemMessageType(message); 163 163 const text = message.record.text; 164 164 165 - if (!type) { 166 - return metadata; 167 - } 165 + if (!type) return metadata; 168 166 169 167 switch (type) { 170 168 case "stream_end": {
+7 -21
js/components/src/lib/theme/theme.tsx
··· 397 397 398 398 // Determine if dark mode should be active 399 399 const isDark = useMemo(() => { 400 - if (forcedTheme === "light") { 401 - return false; 402 - } 403 - if (forcedTheme === "dark") { 404 - return true; 405 - } 406 - if (currentTheme === "light") { 407 - return false; 408 - } 409 - if (currentTheme === "dark") { 410 - return true; 411 - } 412 - if (currentTheme === "system") { 413 - return systemColorScheme === "dark"; 414 - } 400 + if (forcedTheme === "light") return false; 401 + if (forcedTheme === "dark") return true; 402 + if (currentTheme === "light") return false; 403 + if (currentTheme === "dark") return true; 404 + if (currentTheme === "system") return systemColorScheme === "dark"; 415 405 return systemColorScheme === "dark"; 416 406 }, [forcedTheme, currentTheme, systemColorScheme]); 417 407 ··· 454 444 const toggleTheme = () => { 455 445 if (!forcedTheme) { 456 446 setCurrentTheme((prev) => { 457 - if (prev === "light") { 458 - return "dark"; 459 - } 460 - if (prev === "dark") { 461 - return "system"; 462 - } 447 + if (prev === "light") return "dark"; 448 + if (prev === "dark") return "system"; 463 449 return "light"; 464 450 }); 465 451 }
+8 -24
js/components/src/lib/utils.ts
··· 48 48 }, 49 49 screenWidth: number, 50 50 ): T { 51 - if (screenWidth >= 1280 && values.xl !== undefined) { 52 - return values.xl; 53 - } 54 - if (screenWidth >= 1024 && values.lg !== undefined) { 55 - return values.lg; 56 - } 57 - if (screenWidth >= 768 && values.md !== undefined) { 58 - return values.md; 59 - } 60 - if (screenWidth >= 640 && values.sm !== undefined) { 61 - return values.sm; 62 - } 51 + if (screenWidth >= 1280 && values.xl !== undefined) return values.xl; 52 + if (screenWidth >= 1024 && values.lg !== undefined) return values.lg; 53 + if (screenWidth >= 768 && values.md !== undefined) return values.md; 54 + if (screenWidth >= 640 && values.sm !== undefined) return values.sm; 63 55 return values.default; 64 56 } 65 57 ··· 74 66 }): Style { 75 67 const Platform = require("react-native").Platform; 76 68 77 - if (Platform.OS === "ios" && styles.ios) { 78 - return styles.ios; 79 - } 80 - if (Platform.OS === "android" && styles.android) { 81 - return styles.android; 82 - } 83 - if (Platform.OS === "web" && styles.web) { 84 - return styles.web; 85 - } 69 + if (Platform.OS === "ios" && styles.ios) return styles.ios; 70 + if (Platform.OS === "android" && styles.android) return styles.android; 71 + if (Platform.OS === "web" && styles.web) return styles.web; 86 72 return styles.default || {}; 87 73 } 88 74 ··· 91 77 */ 92 78 export function hexToRgba(hex: string, alpha: number = 1): string { 93 79 const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 94 - if (!result) { 95 - return hex; 96 - } 80 + if (!result) return hex; 97 81 98 82 const r = parseInt(result[1], 16); 99 83 const g = parseInt(result[2], 16);
+1 -3
js/components/src/livestream-provider/index.tsx
··· 62 62 const prevActiveTeleportRef = useRef(activeTeleport); 63 63 64 64 useEffect(() => { 65 - if (!activeTeleport || !profile[activeTeleport.streamer]) { 66 - return; 67 - } 65 + if (!activeTeleport || !profile[activeTeleport.streamer]) return; 68 66 69 67 const startsAt = new Date(activeTeleport.startsAt); 70 68 const now = new Date();
+1 -3
js/components/src/livestream-store/stream-key.tsx
··· 25 25 const [error, setError] = useState<string | null>(null); 26 26 27 27 useEffect(() => { 28 - if (key) { 29 - return; 30 - } // already have key 28 + if (key) return; // already have key 31 29 32 30 const generateKey = async () => { 33 31 if (!pdsAgent) {
+4 -12
js/components/src/streamplace-store/branding.tsx
··· 38 38 ]; 39 39 40 40 function getMetaContent(key: string): BrandingAsset | null { 41 - if (typeof window === "undefined" || !window.document) { 42 - return null; 43 - } 41 + if (typeof window === "undefined" || !window.document) return null; 44 42 const meta = document.querySelector(`meta[name="internal-brand:${key}`); 45 43 if (meta && meta.getAttribute("content")) { 46 44 let content = meta.getAttribute("content"); 47 - if (content) { 48 - return JSON.parse(content) as BrandingAsset; 49 - } 45 + if (content) return JSON.parse(content) as BrandingAsset; 50 46 } 51 47 52 48 return null; ··· 69 65 // hrmmmmmmmmmmmm 70 66 if (meta && meta.getAttribute("content")) { 71 67 let content = meta.getAttribute("content"); 72 - if (content) { 73 - acc[key] = JSON.parse(content) as BrandingAsset; 74 - } 68 + if (content) acc[key] = JSON.parse(content) as BrandingAsset; 75 69 } 76 70 return acc; 77 71 }, ··· 120 114 121 115 return useCallback( 122 116 async ({ force = true } = {}) => { 123 - if (!broadcasterDID) { 124 - return; 125 - } 117 + if (!broadcasterDID) return; 126 118 127 119 try { 128 120 store.setState({ brandingLoading: true });
+1 -3
js/components/src/streamplace-store/stream.tsx
··· 26 26 pdsAgent: StreamplaceAgent, 27 27 customThumbnail?: Blob, 28 28 ) => { 29 - if (!customThumbnail) { 30 - return undefined; 31 - } 29 + if (!customThumbnail) return undefined; 32 30 33 31 abortRef.current = new AbortController(); 34 32 const { signal } = abortRef.current;
+1 -3
js/components/src/streamplace-store/xrpc.tsx
··· 10 10 // - SessionManager when logged in 11 11 return useMemo(() => { 12 12 if (!oauthSession) { 13 - if (oauthSession === undefined) { 14 - return null; 15 - } 13 + if (oauthSession === undefined) return null; 16 14 // TODO: change once we allow unauthed requests + profile indexing 17 15 // it's bluesky's AppView b/c otherwise we'd have goosewithpipe.jpg 18 16 // showing up everywhere
+1 -3
js/desktop/src/updater.ts
··· 110 110 }; 111 111 112 112 dialog.showMessageBox(dialogOpts).then(({ response }) => { 113 - if (response === 0) { 114 - autoUpdater.quitAndInstall(); 115 - } 113 + if (response === 0) autoUpdater.quitAndInstall(); 116 114 }); 117 115 }, 118 116 );
-1
package.json
··· 34 34 "lerna": "^8.2.2", 35 35 "lint-staged": "^15.2.10", 36 36 "prettier": "3.4.2", 37 - "prettier-plugin-curly": "^0.4.1", 38 37 "typescript": "^5.8.3" 39 38 }, 40 39 "workspaces": [
+1 -15
pnpm-lock.yaml
··· 42 42 prettier: 43 43 specifier: 3.4.2 44 44 version: 3.4.2 45 - prettier-plugin-curly: 46 - specifier: ^0.4.1 47 - version: 0.4.1(prettier@3.4.2) 48 45 typescript: 49 46 specifier: ^5.8.3 50 47 version: 5.8.3 ··· 7822 7819 glob@8.1.0: 7823 7820 resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} 7824 7821 engines: {node: '>=12'} 7825 - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me 7822 + deprecated: Glob versions prior to v9 are no longer supported 7826 7823 7827 7824 glob@9.3.5: 7828 7825 resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} 7829 7826 engines: {node: '>=16 || 14 >=14.17'} 7830 - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me 7831 7827 7832 7828 global-agent@3.0.0: 7833 7829 resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} ··· 10645 10641 prelude-ls@1.2.1: 10646 10642 resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} 10647 10643 engines: {node: '>= 0.8.0'} 10648 - 10649 - prettier-plugin-curly@0.4.1: 10650 - resolution: {integrity: sha512-Xc7zatoD0/08zYFv+hwnlqT5ekM81DCbBr73CWAsr1Fmx7qLQT/M0wfPx6w/+zfnmXH009xYvjzLUPcwzq7Fbw==} 10651 - engines: {node: '>=18'} 10652 - peerDependencies: 10653 - prettier: ^3 10654 10644 10655 10645 prettier-plugin-organize-imports@4.1.0: 10656 10646 resolution: {integrity: sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==} ··· 26943 26933 tunnel-agent: 0.6.0 26944 26934 26945 26935 prelude-ls@1.2.1: {} 26946 - 26947 - prettier-plugin-curly@0.4.1(prettier@3.4.2): 26948 - dependencies: 26949 - prettier: 3.4.2 26950 26936 26951 26937 prettier-plugin-organize-imports@4.1.0(prettier@3.4.2)(typescript@5.8.3): 26952 26938 dependencies: