Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions plugin/bin/setup.luau
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@ local Charm = require(Packages.Charm)
local Telemetry = require(Plugin.Source.Telemetry.fireEventAsync)
local Types = require(Plugin.Source.Telemetry.Types)
local createPluginLoader = require(script.Parent.createPluginLoader)
local setupGuest = require(script.Parent.setupGuest)

export type PluginMain = (plugin: Plugin, pluginLoaderContext: createPluginLoader.PluginLoaderContext) -> () -> ()

local function setup(plugin: Plugin, main: PluginMain)
plugin.Name = "StudioActivity"

-- Currently, the plugin breaks in weird ways when running in Play Solo.
-- While I come up with a better long-term solution, we're just going to
-- block the plugin from running if we're in an active DataModel.
-- See: https://github.com/grilme99/studio-activity/issues/6
-- Roblox loads this plugin into both the edit data model and any active
-- play-test data models (Play Solo spawns a server + client pair). The
-- edit instance owns the UI, presence pipeline, and Discord sessions.
-- Play-test instances run a stripped-down guest that just reports their
-- liveness back to the host via PluginConnectionService.
if RunService:IsRunning() then
setupGuest(plugin)
return
end

Expand All @@ -38,6 +41,12 @@ local function setup(plugin: Plugin, main: PluginMain)
local PresenceManager = require(Plugin.Source.PresenceManager)
local presenceManager = PresenceManager.get()

-- Initialize PluginContextStore so it begins tracking the active script
-- and any play-test data models before the user opens the UI. This must
-- happen before ProfileStore so the build effect sees a populated context.
local PluginContextStore = require(Plugin.Source.PluginContextStore)
local pluginContextStore = PluginContextStore.get()

-- Initialize ProfileStore early so it loads the active profile from disk
-- and begins resolving it against PlaceContext.
local ProfileStore = require(Plugin.Source.PresenceManager.ProfileStore)
Expand Down Expand Up @@ -128,6 +137,7 @@ local function setup(plugin: Plugin, main: PluginMain)
disposeProfileEffect()
profileStore.destroy()
presenceManager.destroy()
pluginContextStore.destroy()
pluginLoaderContext.destroy()
end)

Expand Down
108 changes: 108 additions & 0 deletions plugin/bin/setupGuest.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
--[[
Lightweight entry point used when the plugin loads inside a play-test
data model rather than the edit data model.

Play Solo creates two test data models (one server, one client) that
each receive their own copy of the plugin. We don't run the full UI,
auth, or presence pipeline in those data models; we just open a
`PluginConnection` to the edit data model and tell the host we're
running, then sit quietly until the data model is torn down.

The host treats the existence of any connected `PluginConnectionTargetType.Test`
connection as "play testing", so messages here are best-effort
enrichment (role: server/client) rather than load-bearing signals.
]]

local PluginConnectionService = game:GetService("PluginConnectionService")
local RunService = game:GetService("RunService")

local Plugin = script:FindFirstAncestor("StudioActivity")

local Logger = require(Plugin.Source.Logger)

local EDIT_TYPE = Enum.PluginConnectionTargetType.Edit

type Role = "server" | "client" | "unknown"

local function detectRole(): Role
if RunService:IsServer() then
return "server"
elseif RunService:IsClient() then
return "client"
end
return "unknown"
end

local function sendStarted(connection: PluginConnection, role: Role)
if not connection.Connected then
return
end
local ok, err = pcall(function()
connection:SendMessage({
kind = "playtestStarted",
role = role,
})
end)
if not ok then
Logger:Warn(`setupGuest: failed to send playtestStarted: {tostring(err)}`)
end
end

local function sendStopped(connection: PluginConnection)
if not connection.Connected then
return
end
pcall(function()
connection:SendMessage({ kind = "playtestStopped" })
end)
end

local function setupGuest(plugin: Plugin)
plugin.Name = "StudioActivity"

if not PluginConnectionService:CanHaveConnectionType(EDIT_TYPE) then
Logger:Info("setupGuest: data model cannot have an Edit connection; skipping")
return
end

local role: Role = detectRole()
Logger:Info(`setupGuest: starting guest mode with role={role}`)

-- Track all connections we've already announced ourselves to, so we don't
-- double-send if the Connected event fires for the same connection.
local announced: { [string]: PluginConnection } = {}

local function announce(connection: PluginConnection)
local targetId = connection.TargetId
if announced[targetId] then
return
end
announced[targetId] = connection
sendStarted(connection, role)
end

-- Subscribe first so we don't miss any Edit connection that arrives
-- between subscribe and enumerate.
local connectedConn = PluginConnectionService.Connected:Connect(function(connection: PluginConnection)
if connection.Type == EDIT_TYPE then
announce(connection)
end
end)

for _, existing in PluginConnectionService:GetPluginConnectionsOfType(EDIT_TYPE) do
announce(existing)
end

plugin.Unloading:Connect(function()
-- Best-effort stopped notification. The connection dropping is the
-- canonical signal; this is just a hint that may arrive faster.
for _, connection in announced do
sendStopped(connection)
end

connectedConn:Disconnect()
table.clear(announced)
end)
end

return setupGuest
63 changes: 44 additions & 19 deletions plugin/src/ActivityPreview/ActivityDetails.luau
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,65 @@ local Types = require(script.Parent.Types)

local View = Foundation.View
local Text = Foundation.Text
local Skeleton = Foundation.Skeleton
local Radius = Foundation.Enums.Radius

local createNextOrder = ReactUtils.createNextOrder

local e = React.createElement

type Props = {
activity: Types.Activity,
activity: Types.Activity?,
LayoutOrder: number?,
}

local function ActivityDetails(props: Props)
local nextOrder = createNextOrder()
local activity = props.activity

return e(View, {
tag = "size-full-0 auto-y col gap-xxsmall",
tag = {
["size-full-0 auto-y col"] = true,
["gap-xxsmall"] = props.activity ~= nil,
["gap-xsmall"] = props.activity == nil,
},
LayoutOrder = props.LayoutOrder,
}, {
Name = e(Text, {
tag = "size-full-0 auto-y text-align-x-left text-wrap text-title-medium content-emphasis",
Text = props.activity.name,
LayoutOrder = nextOrder(),
}),

Details = props.activity.details and e(Text, {
tag = "size-full-0 auto-y text-align-x-left text-wrap text-body-small content-default",
Text = props.activity.details,
LayoutOrder = nextOrder(),
}),

State = props.activity.state and e(Text, {
tag = "size-full-0 auto-y text-align-x-left text-wrap text-body-small content-default",
Text = props.activity.state,
LayoutOrder = nextOrder(),
}),
Name = if activity
then e(Text, {
tag = "size-full-0 auto-y text-align-x-left text-wrap text-title-medium content-emphasis",
Text = activity.name,
LayoutOrder = nextOrder(),
})
else e(Skeleton, {
radius = Radius.XSmall,
Size = UDim2.new(1, 0, 0, 14),
LayoutOrder = nextOrder(),
}),

Details = if activity
then activity.details and e(Text, {
tag = "size-full-0 auto-y text-align-x-left text-wrap text-body-small content-default",
Text = activity.details,
LayoutOrder = nextOrder(),
})
else e(Skeleton, {
radius = Radius.XSmall,
Size = UDim2.new(0.45, 0, 0, 12),
LayoutOrder = nextOrder(),
}),

State = if activity
then activity.state and e(Text, {
tag = "size-full-0 auto-y text-align-x-left text-wrap text-body-small content-default",
Text = activity.state,
LayoutOrder = nextOrder(),
})
else e(Skeleton, {
radius = Radius.XSmall,
Size = UDim2.new(0.6, 0, 0, 12),
LayoutOrder = nextOrder(),
}),
})
end

Expand Down
26 changes: 19 additions & 7 deletions plugin/src/ActivityPreview/ActivityImage.luau
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ local WebImage = require(Plugin.Source.Common.WebImage)

local View = Foundation.View
local Tooltip = Foundation.Tooltip
local Skeleton = Foundation.Skeleton
local PopoverSide = Foundation.Enums.PopoverSide
local PopoverAlign = Foundation.Enums.PopoverAlign
local Radius = Foundation.Enums.Radius
local useTokens = Foundation.Hooks.useTokens

local useSignalState = ReactCharm.useSignalState
Expand Down Expand Up @@ -40,20 +42,30 @@ local function ActivityImage(props: Props)
local largeImageUrl = if rawLargeImage then appAssets.resolveUrl(rawLargeImage) else nil
local smallImageUrl = if rawSmallImage then appAssets.resolveUrl(rawSmallImage) else nil

local largeBlock = largeImageUrl
and e(View, {
local largeBlock = if largeImageUrl
then e(View, {
tag = "size-full",
}, {
Image = e(WebImage, {
url = largeImageUrl,
size = "big",
}),
})
else e(Skeleton, {
radius = Radius.Small,
Size = UDim2.fromScale(1, 1),
})

local smallImageBlock = smallImageUrl and e(WebImage, {
url = smallImageUrl,
size = "small",
})
local smallImageBlock = if smallImageUrl
then e(WebImage, {
url = smallImageUrl,
size = "small",
})
elseif rawSmallImage then e(Skeleton, {
radius = Radius.Circle,
Size = UDim2.fromScale(1, 1),
})
else nil

return e(View, {
tag = "size-1600-1600",
Expand All @@ -67,7 +79,7 @@ local function ActivityImage(props: Props)
}, largeBlock)
else largeBlock,

SmallImageBlock = smallImageUrl and e(View, {
SmallImageBlock = rawSmallImage and e(View, {
tag = "size-700-700 bg-surface-300 radius-circle",
stroke = {
Color = tokens.Color.Surface.Surface_300.Color3,
Expand Down
13 changes: 7 additions & 6 deletions plugin/src/ActivityPreview/ActivityPreview.luau
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ local createNextOrder = ReactUtils.createNextOrder
local e = React.createElement

type Props = {
activity: Types.Activity,
activity: Types.Activity?,
LayoutOrder: number?,
}

local function ActivityPreview(props: Props)
local nextOrder = createNextOrder()
local activity = props.activity

return e(View, {
tag = "size-full-0 auto-y bg-surface-300 clip col padding-small gap-medium radius-small stroke-emphasis stroke-thick",
Expand All @@ -50,7 +51,7 @@ local function ActivityPreview(props: Props)
LayoutOrder = nextOrder(),
}, {
ActivityImage = e(ActivityImage, {
assets = props.activity.assets,
assets = if activity then activity.assets else nil,
LayoutOrder = nextOrder(),
}),

Expand All @@ -59,19 +60,19 @@ local function ActivityPreview(props: Props)
LayoutOrder = nextOrder(),
}, {
ActivityDetails = e(ActivityDetails, {
activity = props.activity,
activity = activity,
LayoutOrder = nextOrder(),
}),

ActivityTimer = e(ActivityTimer, {
startedAt = props.activity.startedAt,
startedAt = if activity then activity.startedAt else nil,
LayoutOrder = nextOrder(),
}),
}),
}),

ActivityButtons = (props.activity.buttons and #props.activity.buttons > 0) and e(ActivityButtons, {
buttons = props.activity.buttons,
ActivityButtons = (activity and activity.buttons and #activity.buttons > 0) and e(ActivityButtons, {
buttons = activity.buttons,
LayoutOrder = nextOrder(),
}),
})
Expand Down
22 changes: 15 additions & 7 deletions plugin/src/ActivityPreview/ActivityTimer.luau
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ local useTimerText = require(script.Parent.useTimerText)
local View = Foundation.View
local Text = Foundation.Text
local Icon = Foundation.Icon
local Skeleton = Foundation.Skeleton
local IconName = Foundation.Enums.IconName
local IconVariant = Foundation.Enums.IconVariant
local IconSize = Foundation.Enums.IconSize
local Radius = Foundation.Enums.Radius
local useTokens = Foundation.Hooks.useTokens

local createNextOrder = ReactUtils.createNextOrder
Expand All @@ -29,7 +31,7 @@ local function ActivityTimer(props: Props)
local nextOrder = createNextOrder()

local tokens = useTokens()
local contentColor = tokens.Color.Extended.Green.Green_500
local contentColor = if props.startedAt then tokens.Color.Extended.Green.Green_500 else tokens.Color.System.Neutral

local startTime = useMemo(function()
return props.startedAt or os.time()
Expand All @@ -49,12 +51,18 @@ local function ActivityTimer(props: Props)
LayoutOrder = nextOrder(),
}),

TimerText = e(Text, {
tag = "auto-xy text-body-small",
textStyle = contentColor,
Text = timerText,
LayoutOrder = nextOrder(),
}),
TimerText = if props.startedAt
then e(Text, {
tag = "auto-xy text-body-small",
textStyle = contentColor,
Text = timerText,
LayoutOrder = nextOrder(),
})
else e(Skeleton, {
radius = Radius.XSmall,
Size = UDim2.fromOffset(42, 12),
LayoutOrder = nextOrder(),
}),
})
end

Expand Down
Loading
Loading