2024-04-17 14:29:47 -04:00
/ *
* Vencord , a Discord client mod
* Copyright ( c ) 2024 Vendicated and contributors
* SPDX - License - Identifier : GPL - 3.0 - or - later
* /
2024-06-01 14:32:22 -04:00
import { definePluginSettings } from "@api/Settings" ;
2024-04-17 14:29:47 -04:00
import { classNameFactory } from "@api/Styles" ;
2024-06-01 14:32:22 -04:00
import ErrorBoundary from "@components/ErrorBoundary" ;
2024-04-17 14:29:47 -04:00
import { Flex } from "@components/Flex" ;
2024-04-18 19:15:01 -04:00
import { EquicordDevs } from "@utils/constants" ;
2024-06-01 14:32:22 -04:00
import { openUserProfile } from "@utils/discord" ;
import { Margins } from "@utils/margins" ;
import { classes } from "@utils/misc" ;
import definePlugin , { OptionType } from "@utils/types" ;
2024-07-20 03:55:47 -04:00
import { findByPropsLazy , findComponentByCodeLazy } from "@webpack" ;
import { ApplicationStreamingStore , Clickable , Forms , i18n , RelationshipStore , Tooltip , UserStore , useStateFromStores } from "@webpack/common" ;
2024-06-01 14:32:22 -04:00
import { User } from "discord-types/general" ;
2024-04-17 14:29:47 -04:00
interface WatchingProps {
userIds : string [ ] ;
guildId? : string ;
}
const cl = classNameFactory ( "whosWatching-" ) ;
function getUsername ( user : any ) : string {
return RelationshipStore . getNickname ( user . id ) || user . globalName || user . username ;
}
2024-06-01 14:32:22 -04:00
const settings = definePluginSettings ( {
showPanel : {
description : "Show spectators under screenshare panel" ,
type : OptionType . BOOLEAN ,
default : true ,
restartNeeded : true
} ,
} ) ;
2024-06-30 20:31:49 -04:00
function encodeStreamKey ( stream ) {
const { streamType , guildId , channelId , ownerId } = stream ;
switch ( streamType ) {
case "guild" :
return [ streamType , guildId , channelId , ownerId ] . join ( ":" ) ;
case "call" :
return [ streamType , channelId , ownerId ] . join ( ":" ) ;
default :
throw console . log ( "Unknown stream type " . concat ( streamType ) ) ;
}
}
2024-04-17 14:29:47 -04:00
function Watching ( { userIds , guildId } : WatchingProps ) : JSX . Element {
// Missing Users happen when UserStore.getUser(id) returns null -- The client should automatically cache spectators, so this might not be possible but it's better to be sure just in case
let missingUsers = 0 ;
const users = userIds . map ( id = > UserStore . getUser ( id ) ) . filter ( user = > Boolean ( user ) ? true : ( missingUsers += 1 , false ) ) ;
return (
< div className = { cl ( "content" ) } >
{ userIds . length ?
( < >
< Forms.FormTitle > { i18n . Messages . SPECTATORS . format ( { numViewers : userIds.length } ) } < / Forms.FormTitle >
< Flex flexDirection = "column" style = { { gap : 6 } } >
{ users . map ( user = > (
< Flex flexDirection = "row" style = { { gap : 6 , alignContent : "center" } } className = { cl ( "user" ) } >
< img src = { user . getAvatarURL ( guildId ) } style = { { borderRadius : 8 , width : 16 , height : 16 } } / >
{ getUsername ( user ) }
< / Flex >
) ) }
{ missingUsers > 0 && < span className = { cl ( "more_users" ) } > { ` + ${ i18n . Messages . NUM_USERS . format ( { num : missingUsers } )} ` } < / span > }
< / Flex >
< / > )
: ( < span className = { cl ( "no_viewers" ) } > No spectators < / span > ) }
< / div >
) ;
}
2024-06-01 14:32:22 -04:00
const UserSummaryItem = findComponentByCodeLazy ( "defaultRenderUser" , "showDefaultAvatarsForNullUsers" ) ;
const AvatarStyles = findByPropsLazy ( "moreUsers" , "emptyUser" , "avatarContainer" , "clickableAvatar" ) ;
2024-04-17 14:29:47 -04:00
export default definePlugin ( {
name : "WhosWatching" ,
2024-06-01 14:32:22 -04:00
description : "Hover over the screenshare icon to view what users are watching your stream" ,
2024-07-07 00:48:27 -04:00
authors : [ EquicordDevs . Fres ] ,
2024-06-01 14:32:22 -04:00
settings : settings ,
2024-04-17 14:29:47 -04:00
patches : [
{
find : ".Masks.STATUS_SCREENSHARE,width:32" ,
replacement : {
2024-06-21 20:05:37 -04:00
match : /(\i):function\(\)\{return (\i)\}/ ,
replace : "$1:function(){return $self.component({OriginalComponent:$2})}"
2024-04-17 14:29:47 -04:00
}
2024-06-01 14:32:22 -04:00
} ,
{
predicate : ( ) = > settings . store . showPanel ,
find : "this.isJoinableActivity()||" ,
replacement : {
match : /(this\.isJoinableActivity\(\).{0,200}children:.{0,50})"div"/ ,
replace : "$1$self.WrapperComponent"
}
2024-04-17 14:29:47 -04:00
}
] ,
2024-06-01 14:32:22 -04:00
WrapperComponent : ErrorBoundary.wrap ( props = > {
const stream = useStateFromStores ( [ ApplicationStreamingStore ] , ( ) = > ApplicationStreamingStore . getCurrentUserActiveStream ( ) ) ;
if ( ! stream ) return < div { ...props } > { props . children } < / div > ;
2024-07-20 03:55:47 -04:00
const userIds = ApplicationStreamingStore . getViewerIds ( stream ) ;
2024-06-01 14:32:22 -04:00
let missingUsers = 0 ;
const users = userIds . map ( id = > UserStore . getUser ( id ) ) . filter ( user = > Boolean ( user ) ? true : ( missingUsers += 1 , false ) ) ;
function renderMoreUsers ( _label : string , count : number ) {
const sliced = users . slice ( count - 1 ) ;
return (
< Tooltip text = { < Watching userIds = { userIds } guildId = { stream . guildId } / > } >
{ ( { onMouseEnter , onMouseLeave } ) = > (
< div
className = { AvatarStyles . moreUsers }
onMouseEnter = { onMouseEnter }
onMouseLeave = { onMouseLeave }
>
+ { sliced . length + missingUsers }
< / div >
) }
< / Tooltip >
) ;
}
return (
< >
< div { ...props } > { props . children } < / div >
< div className = { classes ( cl ( "spectators_panel" ) , Margins . top8 ) } >
{ users . length ?
< >
< Forms.FormTitle tag = "h3" style = { { marginTop : 8 , marginBottom : 0 , textTransform : "uppercase" } } > { i18n . Messages . SPECTATORS . format ( { numViewers : userIds.length } ) } < / Forms.FormTitle >
< UserSummaryItem
users = { users }
count = { userIds . length }
renderIcon = { false }
max = { 12 }
showDefaultAvatarsForNullUsers
showUserPopout
guildId = { stream . guildId }
renderMoreUsers = { renderMoreUsers }
renderUser = { ( user : User ) = > (
< Clickable
className = { AvatarStyles . clickableAvatar }
onClick = { ( ) = > openUserProfile ( user . id ) }
>
< img
className = { AvatarStyles . avatar }
src = { user . getAvatarURL ( void 0 , 80 , true ) }
alt = { user . username }
title = { user . username }
/ >
< / Clickable >
) }
/ >
< / >
: < Forms.FormText > No spectators < / Forms.FormText >
}
< / div >
< / >
) ;
} ) ,
2024-04-17 14:29:47 -04:00
component : function ( { OriginalComponent } ) {
2024-07-20 02:57:17 -04:00
return ErrorBoundary . wrap ( ( props : any ) = > {
2024-04-17 14:29:47 -04:00
const stream = useStateFromStores ( [ ApplicationStreamingStore ] , ( ) = > ApplicationStreamingStore . getCurrentUserActiveStream ( ) ) ;
2024-07-20 03:55:47 -04:00
const viewers = ApplicationStreamingStore . getViewerIds ( stream ) ;
2024-04-17 14:29:47 -04:00
return < Tooltip text = { < Watching userIds = { viewers } guildId = { stream . guildId } / > } >
{ ( { onMouseEnter , onMouseLeave } ) = > (
< div onMouseEnter = { onMouseEnter } onMouseLeave = { onMouseLeave } >
< OriginalComponent { ...props } / >
< / div >
) }
< / Tooltip > ;
2024-07-20 02:57:17 -04:00
} ) ;
2024-04-17 14:29:47 -04:00
}
} ) ;