From 5270c80862fe24bba326505345a3a8e924ed1a02 Mon Sep 17 00:00:00 2001 From: Brooke Rhodes Date: Tue, 19 May 2026 00:17:53 -0400 Subject: [PATCH 1/7] buttons! --- .lute/build.luau | 2 +- plugin/src/ActivityPreview/Types.luau | 6 ++++++ plugin/src/PlaceDetailsStore/init.luau | 2 +- plugin/src/Plugin/LocalStorageStore.luau | 6 ++++++ .../ProfileStore/buildCustomActivity.luau | 1 + plugin/src/PresenceManager/ProfileStore/presets.luau | 5 +++++ .../src/PresenceManager/createPresenceManager.luau | 12 ++++++++++++ 7 files changed, 32 insertions(+), 2 deletions(-) diff --git a/.lute/build.luau b/.lute/build.luau index 819a9f2..d03d7e0 100644 --- a/.lute/build.luau +++ b/.lute/build.luau @@ -100,7 +100,7 @@ local context: BuildContext = { api = { secure = channel == "prod", - host = process.env.API_HOST, + host = if channel == "prod" then process.env.API_HOST else "localhost:8787", }, discord = { diff --git a/plugin/src/ActivityPreview/Types.luau b/plugin/src/ActivityPreview/Types.luau index a36b898..56280e6 100644 --- a/plugin/src/ActivityPreview/Types.luau +++ b/plugin/src/ActivityPreview/Types.luau @@ -1,3 +1,8 @@ +export type ActivityButton = { + label: string, + url: string, +} + export type ActivityAssets = { largeImage: string?, largeText: string?, @@ -11,6 +16,7 @@ export type Activity = { detailsUrl: string?, state: string?, stateUrl: string?, + buttons: { ActivityButton }?, startedAt: number?, assets: ActivityAssets?, } diff --git a/plugin/src/PlaceDetailsStore/init.luau b/plugin/src/PlaceDetailsStore/init.luau index 1913d95..067b3ff 100644 --- a/plugin/src/PlaceDetailsStore/init.luau +++ b/plugin/src/PlaceDetailsStore/init.luau @@ -15,6 +15,6 @@ export type PlaceDetailsStore = createPlaceDetailsStore.PlaceDetailsStore return table.freeze({ get = Charm.computed(function() - return createPlaceDetailsStore() + return Charm.untracked(createPlaceDetailsStore) end), }) diff --git a/plugin/src/Plugin/LocalStorageStore.luau b/plugin/src/Plugin/LocalStorageStore.luau index e2f6533..9d410d3 100644 --- a/plugin/src/Plugin/LocalStorageStore.luau +++ b/plugin/src/Plugin/LocalStorageStore.luau @@ -10,6 +10,11 @@ local createPluginSettingsStore = require(script.Parent.createPluginSettingsStor local DiscordApi = require(Plugin.Source.Api.Discord) type ApiUser = DiscordApi.User +export type CustomProfileButton = { + label: string, + url: string, +} + --[[ NOTE: All optional fields here are intentionally tri-state — `nil` means "use the runtime default" (which can differ per field), and `true`/`false` @@ -20,6 +25,7 @@ type ApiUser = DiscordApi.User type CustomProfileActivity = { details: string?, state: string?, + buttons: { CustomProfileButton }?, showPlaceIcon: boolean?, overridePlaceId: number?, } diff --git a/plugin/src/PresenceManager/ProfileStore/buildCustomActivity.luau b/plugin/src/PresenceManager/ProfileStore/buildCustomActivity.luau index 1fbc0ab..e1fd310 100644 --- a/plugin/src/PresenceManager/ProfileStore/buildCustomActivity.luau +++ b/plugin/src/PresenceManager/ProfileStore/buildCustomActivity.luau @@ -31,6 +31,7 @@ local function buildCustomActivity(context: PlaceContext, profile: CustomProfile details = if details then interpolate(details, templates) else nil, detailsUrl = "https://github.com/grilme99/studio-activity", state = if state then interpolate(state, templates) else nil, + buttons = profile.activity.buttons, assets = if profile.activity.showPlaceIcon and onlineDetails then { largeImage = onlineDetails.iconWebUrl, diff --git a/plugin/src/PresenceManager/ProfileStore/presets.luau b/plugin/src/PresenceManager/ProfileStore/presets.luau index 3b728af..122a0bc 100644 --- a/plugin/src/PresenceManager/ProfileStore/presets.luau +++ b/plugin/src/PresenceManager/ProfileStore/presets.luau @@ -36,6 +36,11 @@ local presets: { [string]: PresetProfile } = { elseif offlineDetails then offlineDetails.placeName else nil, stateUrl = if onlineDetails then onlineDetails.placeLink else nil, + buttons = if onlineDetails + then { + { label = "Play on Roblox", url = onlineDetails.placeLink }, + } + else nil, assets = if onlineDetails then { largeImage = onlineDetails.iconWebUrl, diff --git a/plugin/src/PresenceManager/createPresenceManager.luau b/plugin/src/PresenceManager/createPresenceManager.luau index bd4e601..b5d25f9 100644 --- a/plugin/src/PresenceManager/createPresenceManager.luau +++ b/plugin/src/PresenceManager/createPresenceManager.luau @@ -32,12 +32,18 @@ export type ActivityConfigAssets = { smallText: string?, } +export type ActivityConfigButton = { + label: string, + url: string, +} + export type ActivityConfig = { name: string, details: string?, detailsUrl: string?, state: string?, stateUrl: string?, + buttons: { ActivityConfigButton }?, startedAt: number?, assets: ActivityConfigAssets?, } @@ -56,6 +62,10 @@ export type PresenceManager = { --- Convert ActivityConfig into the API format (mostly changing to snake_case.) local function serializeActivity(activity: ActivityConfig, startedAt: number?): ApiActivity + if activity.buttons and #activity.buttons > 2 then + Logger:Warn("Activities only support up to 2 buttons") + end + local apiActivity: ApiActivity = { type = 0, -- PLAYING name = activity.name, @@ -66,6 +76,8 @@ local function serializeActivity(activity: ActivityConfig, startedAt: number?): state = activity.state, state_url = activity.stateUrl, + buttons = activity.buttons, + application_id = BuildVars.discord.clientId, platform = "desktop", } From 8b94055f760eb1f7f9aabfbc4e48a649b680a6c4 Mon Sep 17 00:00:00 2001 From: Brooke Rhodes Date: Tue, 19 May 2026 00:31:12 -0400 Subject: [PATCH 2/7] show custom buttons on preview --- .../src/ActivityPreview/ActivityPreview.luau | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/plugin/src/ActivityPreview/ActivityPreview.luau b/plugin/src/ActivityPreview/ActivityPreview.luau index fa01699..57782cb 100644 --- a/plugin/src/ActivityPreview/ActivityPreview.luau +++ b/plugin/src/ActivityPreview/ActivityPreview.luau @@ -13,6 +13,9 @@ local Types = require(script.Parent.Types) local View = Foundation.View local Text = Foundation.Text +local Button = Foundation.Button +local InputSize = Foundation.Enums.InputSize +local ButtonVariant = Foundation.Enums.ButtonVariant local createNextOrder = ReactUtils.createNextOrder @@ -26,6 +29,24 @@ type Props = { local function ActivityPreview(props: Props) local nextOrder = createNextOrder() + local buttonChildren: { React.ReactNode } = {} + if props.activity.buttons then + for index, button in props.activity.buttons do + table.insert( + buttonChildren, + e(Button, { + text = button.label, + key = `{button.label}:{index}`, + size = InputSize.Small, + variant = if index == 1 then ButtonVariant.SoftEmphasis else ButtonVariant.Standard, + width = UDim.new(1, 0), + LayoutOrder = index, + onActivated = function() end, + }) + ) + end + end + return e(View, { tag = "size-full-0 auto-y bg-surface-300 clip col padding-small gap-medium radius-small stroke-emphasis stroke-thick", LayoutOrder = props.LayoutOrder, @@ -68,6 +89,11 @@ local function ActivityPreview(props: Props) }), }), }), + + ActivityButtons = props.activity.buttons and e(View, { + tag = "size-full-0 auto-y col gap-small", + LayoutOrder = nextOrder(), + }, buttonChildren), }) end From ee261693e2684c1b3925360d827f66351f5ba260 Mon Sep 17 00:00:00 2001 From: Brooke Rhodes Date: Tue, 19 May 2026 00:47:36 -0400 Subject: [PATCH 3/7] make singletons more stable --- plugin/src/Api/AppAssetsStore.luau | 3 +- .../src/Authentication/AccountStore/init.luau | 6 +-- plugin/src/Authentication/AuthStore.luau | 4 +- plugin/src/Common/createSingleton.luau | 44 +++++++++++++++++++ plugin/src/Modal/ModalStore/init.luau | 6 +-- .../Notification/NotificationStore/init.luau | 6 +-- plugin/src/PlaceDetailsStore/init.luau | 8 +--- plugin/src/Plugin/LocalStorageStore.luau | 4 +- plugin/src/Plugin/PluginStore/init.luau | 6 +-- .../PresenceManager/ProfileStore/init.luau | 8 +--- plugin/src/PresenceManager/init.luau | 6 +-- .../src/UserSettings/UserSettingsStore.luau | 4 +- 12 files changed, 66 insertions(+), 39 deletions(-) create mode 100644 plugin/src/Common/createSingleton.luau diff --git a/plugin/src/Api/AppAssetsStore.luau b/plugin/src/Api/AppAssetsStore.luau index a80cc31..9d1b8eb 100644 --- a/plugin/src/Api/AppAssetsStore.luau +++ b/plugin/src/Api/AppAssetsStore.luau @@ -6,6 +6,7 @@ local Charm = require(Packages.Charm) local BuildVars = require(Plugin.Source.BuildVars) local DiscordApi = require(Plugin.Source.Api.Discord) local Logger = require(Plugin.Source.Logger) +local createSingleton = require(Plugin.Source.Common.createSingleton) type ApplicationAsset = DiscordApi.ApplicationAsset @@ -60,7 +61,7 @@ local function createAppAssetsStore(discord: DiscordApi.DiscordApi) end return { - get = Charm.computed(function() + get = createSingleton(function() local discord = DiscordApi.new({ clientId = BuildVars.discord.clientId, }) diff --git a/plugin/src/Authentication/AccountStore/init.luau b/plugin/src/Authentication/AccountStore/init.luau index 8acd4da..3a9442f 100644 --- a/plugin/src/Authentication/AccountStore/init.luau +++ b/plugin/src/Authentication/AccountStore/init.luau @@ -1,14 +1,12 @@ local Plugin = script:FindFirstAncestor("StudioActivity") -local Packages = Plugin.Packages -local Charm = require(Packages.Charm) - local BuildVars = require(Plugin.Source.BuildVars) local DiscordApi = require(Plugin.Source.Api.Discord) local createAccountStore = require(script.createAccountStore) +local createSingleton = require(Plugin.Source.Common.createSingleton) return table.freeze({ - get = Charm.computed(function() + get = createSingleton(function() local discord = DiscordApi.new({ clientId = BuildVars.discord.clientId, }) diff --git a/plugin/src/Authentication/AuthStore.luau b/plugin/src/Authentication/AuthStore.luau index 9a933a0..c7bb503 100644 --- a/plugin/src/Authentication/AuthStore.luau +++ b/plugin/src/Authentication/AuthStore.luau @@ -1,10 +1,10 @@ local Plugin = script:FindFirstAncestor("StudioActivity") local Packages = Plugin.Packages -local Charm = require(Packages.Charm) local t = require(Packages.t) local createPluginSettingsStore = require(Plugin.Source.Plugin.createPluginSettingsStore) +local createSingleton = require(Plugin.Source.Common.createSingleton) export type Account = { accessToken: string, @@ -34,7 +34,7 @@ local validate = t.interface({ }) return { - get = Charm.computed(function() + get = createSingleton(function() return createPluginSettingsStore("PresenceAuthStore", defaultValue, validate) end), } diff --git a/plugin/src/Common/createSingleton.luau b/plugin/src/Common/createSingleton.luau new file mode 100644 index 0000000..12433df --- /dev/null +++ b/plugin/src/Common/createSingleton.luau @@ -0,0 +1,44 @@ +local Plugin = script:FindFirstAncestor("StudioActivity") + +local Packages = Plugin.Packages +local Charm = require(Packages.Charm) + +--[[ + Returns a lazy getter for a singleton store. The factory is invoked on + first call and the result is cached forever. The factory runs inside + `Charm.untracked`, so any signal reads or writes performed during the + store's own initialization do NOT register as dependencies of whichever + reactive context (effect / computed / React render) happened to make the + first call. The returned store is still fully reactive at the method + level — its internal signals and computeds keep working as normal. + + Without this, a factory that synchronously reads and writes one of its + own internal signals during init (e.g. `createPlaceDetailsStore` calling + `prefetchPlaceIds(game.PlaceId)`) would link that signal as a dependency + of the calling reactive context, and every subsequent mutation of the + signal would cause the caller to re-evaluate — constructing a brand new + store and feeding back into the same cycle. +]] +local function createSingleton(factory: () -> T): () -> T + local instance: T? = nil + local initializing = false + + return function(): T + if instance ~= nil then + return instance :: T + end + if initializing then + error("createSingleton: recursive factory invocation detected", 2) + end + initializing = true + local ok, result = pcall(Charm.untracked, factory) + initializing = false + if not ok then + error(result, 2) + end + instance = result + return instance :: T + end +end + +return createSingleton diff --git a/plugin/src/Modal/ModalStore/init.luau b/plugin/src/Modal/ModalStore/init.luau index 6902e76..571dcec 100644 --- a/plugin/src/Modal/ModalStore/init.luau +++ b/plugin/src/Modal/ModalStore/init.luau @@ -1,12 +1,10 @@ local Plugin = script:FindFirstAncestor("StudioActivity") -local Packages = Plugin.Packages -local Charm = require(Packages.Charm) - local createModalStore = require(script.createModalStore) +local createSingleton = require(Plugin.Source.Common.createSingleton) export type ModalId = createModalStore.ModalId export type ModalStackEntry = createModalStore.ModalStackEntry return { - get = Charm.computed(createModalStore), + get = createSingleton(createModalStore), } diff --git a/plugin/src/Notification/NotificationStore/init.luau b/plugin/src/Notification/NotificationStore/init.luau index f431231..5a95da6 100644 --- a/plugin/src/Notification/NotificationStore/init.luau +++ b/plugin/src/Notification/NotificationStore/init.luau @@ -1,11 +1,9 @@ local Plugin = script:FindFirstAncestor("StudioActivity") -local Packages = Plugin.Packages -local Charm = require(Packages.Charm) - local createNotificationStore = require(script.createNotificationStore) +local createSingleton = require(Plugin.Source.Common.createSingleton) export type NotificationType = createNotificationStore.NotificationType return { - get = Charm.computed(createNotificationStore), + get = createSingleton(createNotificationStore), } diff --git a/plugin/src/PlaceDetailsStore/init.luau b/plugin/src/PlaceDetailsStore/init.luau index 067b3ff..ce53b0e 100644 --- a/plugin/src/PlaceDetailsStore/init.luau +++ b/plugin/src/PlaceDetailsStore/init.luau @@ -1,9 +1,7 @@ local Plugin = script:FindFirstAncestor("StudioActivity") -local Packages = Plugin.Packages -local Charm = require(Packages.Charm) - local createPlaceDetailsStore = require(script.createPlaceDetailsStore) +local createSingleton = require(Plugin.Source.Common.createSingleton) export type PlaceType = createPlaceDetailsStore.PlaceType export type Status = createPlaceDetailsStore.Status @@ -14,7 +12,5 @@ export type PlaceEntry = createPlaceDetailsStore.PlaceEntry export type PlaceDetailsStore = createPlaceDetailsStore.PlaceDetailsStore return table.freeze({ - get = Charm.computed(function() - return Charm.untracked(createPlaceDetailsStore) - end), + get = createSingleton(createPlaceDetailsStore), }) diff --git a/plugin/src/Plugin/LocalStorageStore.luau b/plugin/src/Plugin/LocalStorageStore.luau index 9d410d3..f51d266 100644 --- a/plugin/src/Plugin/LocalStorageStore.luau +++ b/plugin/src/Plugin/LocalStorageStore.luau @@ -1,11 +1,11 @@ local Plugin = script:FindFirstAncestor("StudioActivity") local Packages = Plugin.Packages -local Charm = require(Packages.Charm) local t = require(Packages.t) local Logger = require(Plugin.Source.Logger) local createPluginSettingsStore = require(script.Parent.createPluginSettingsStore) +local createSingleton = require(Plugin.Source.Common.createSingleton) local DiscordApi = require(Plugin.Source.Api.Discord) type ApiUser = DiscordApi.User @@ -101,7 +101,7 @@ local function validate(value: any): (boolean, string) end return { - get = Charm.computed(function() + get = createSingleton(function() return createPluginSettingsStore("PresenceLocalStorage", defaultValue, validate) end), } diff --git a/plugin/src/Plugin/PluginStore/init.luau b/plugin/src/Plugin/PluginStore/init.luau index 558ef4a..53ee219 100644 --- a/plugin/src/Plugin/PluginStore/init.luau +++ b/plugin/src/Plugin/PluginStore/init.luau @@ -1,10 +1,8 @@ local Plugin = script:FindFirstAncestor("StudioActivity") -local Packages = Plugin.Packages -local Charm = require(Packages.Charm) - local createPluginStore = require(script.createPluginStore) +local createSingleton = require(Plugin.Source.Common.createSingleton) return { - get = Charm.computed(createPluginStore), + get = createSingleton(createPluginStore), } diff --git a/plugin/src/PresenceManager/ProfileStore/init.luau b/plugin/src/PresenceManager/ProfileStore/init.luau index b9e835a..90d2aaa 100644 --- a/plugin/src/PresenceManager/ProfileStore/init.luau +++ b/plugin/src/PresenceManager/ProfileStore/init.luau @@ -1,15 +1,11 @@ local Plugin = script:FindFirstAncestor("StudioActivity") -local Packages = Plugin.Packages -local Charm = require(Packages.Charm) - local createProfileStore = require(script.createProfileStore) +local createSingleton = require(Plugin.Source.Common.createSingleton) export type ProfileStore = createProfileStore.ProfileStore export type Profile = createProfileStore.Profile return table.freeze({ - get = Charm.computed(function() - return createProfileStore() - end), + get = createSingleton(createProfileStore), }) diff --git a/plugin/src/PresenceManager/init.luau b/plugin/src/PresenceManager/init.luau index 1221291..a9e23bb 100644 --- a/plugin/src/PresenceManager/init.luau +++ b/plugin/src/PresenceManager/init.luau @@ -1,18 +1,16 @@ local Plugin = script:FindFirstAncestor("StudioActivity") -local Packages = Plugin.Packages -local Charm = require(Packages.Charm) - local BuildVars = require(Plugin.Source.BuildVars) local DiscordApi = require(Plugin.Source.Api.Discord) local createPresenceManager = require(script.createPresenceManager) +local createSingleton = require(Plugin.Source.Common.createSingleton) export type PresenceManager = createPresenceManager.PresenceManager export type ActivityConfig = createPresenceManager.ActivityConfig export type ActivityConfigAssets = createPresenceManager.ActivityConfigAssets return table.freeze({ - get = Charm.computed(function() + get = createSingleton(function() local discord = DiscordApi.new({ clientId = BuildVars.discord.clientId, }) diff --git a/plugin/src/UserSettings/UserSettingsStore.luau b/plugin/src/UserSettings/UserSettingsStore.luau index a8a27c6..b7165bc 100644 --- a/plugin/src/UserSettings/UserSettingsStore.luau +++ b/plugin/src/UserSettings/UserSettingsStore.luau @@ -1,10 +1,10 @@ local Plugin = script:FindFirstAncestor("StudioActivity") local Packages = Plugin.Packages -local Charm = require(Packages.Charm) local t = require(Packages.t) local createPluginSettingsStore = require(Plugin.Source.Plugin.createPluginSettingsStore) +local createSingleton = require(Plugin.Source.Common.createSingleton) local defaultSettings = require(script.Parent.DefaultSettings) export type UserSettings = { @@ -28,7 +28,7 @@ for key in defaultSettings do end return { - get = Charm.computed(function() + get = createSingleton(function() return createPluginSettingsStore("PresenceUserSettings", defaultValue, validate) end), } From 80119bf80ccb633d2d1a4d0fa49459cdc044d736 Mon Sep 17 00:00:00 2001 From: Brooke Rhodes Date: Tue, 19 May 2026 10:59:02 -0400 Subject: [PATCH 4/7] add dedicated buttons component --- .../src/ActivityPreview/ActivityButtons.luau | 63 +++++++++++++++++++ .../src/ActivityPreview/ActivityPreview.luau | 28 ++------- 2 files changed, 67 insertions(+), 24 deletions(-) create mode 100644 plugin/src/ActivityPreview/ActivityButtons.luau diff --git a/plugin/src/ActivityPreview/ActivityButtons.luau b/plugin/src/ActivityPreview/ActivityButtons.luau new file mode 100644 index 0000000..f91d0da --- /dev/null +++ b/plugin/src/ActivityPreview/ActivityButtons.luau @@ -0,0 +1,63 @@ +local Plugin = script:FindFirstAncestor("StudioActivity") + +local Packages = Plugin.Packages +local Foundation = require(Packages.Foundation) +local React = require(Packages.React) +local ReactUtils = require(Packages.ReactUtils) + +local Types = require(script.Parent.Types) + +local View = Foundation.View +local Button = Foundation.Button +local Tooltip = Foundation.Tooltip +local InputSize = Foundation.Enums.InputSize +local ButtonVariant = Foundation.Enums.ButtonVariant +local FillBehavior = Foundation.Enums.FillBehavior +local PopoverAlign = Foundation.Enums.PopoverAlign +local PopoverSide = Foundation.Enums.PopoverSide + +local createNextOrder = ReactUtils.createNextOrder + +local e = React.createElement + +type Props = { + buttons: { Types.ActivityButton }, + LayoutOrder: number?, +} + +local function ActivityButtons(props: Props) + local nextOrder = createNextOrder() + + local buttonChildren: { React.ReactNode } = {} + for index, button in props.buttons do + table.insert( + buttonChildren, + e(Button, { + text = button.label, + key = `{button.label}:{index}`, + size = InputSize.Small, + variant = if index == 1 then ButtonVariant.SoftEmphasis else ButtonVariant.Standard, + fillBehavior = FillBehavior.Fill, + width = UDim.new(1, 0), + LayoutOrder = index, + onActivated = function() end, + }) + ) + end + + return e(Tooltip, { + title = if #props.buttons == 1 + then "You won't see this button on your own profile" + else "You won't see these buttons on your own profile", + align = PopoverAlign.Center, + side = PopoverSide.Top, + LayoutOrder = props.LayoutOrder, + }, { + ButtonsContainer = e(View, { + tag = "size-full-0 auto-y col gap-small", + LayoutOrder = nextOrder(), + }, buttonChildren), + }) +end + +return ActivityButtons diff --git a/plugin/src/ActivityPreview/ActivityPreview.luau b/plugin/src/ActivityPreview/ActivityPreview.luau index 57782cb..debae47 100644 --- a/plugin/src/ActivityPreview/ActivityPreview.luau +++ b/plugin/src/ActivityPreview/ActivityPreview.luau @@ -5,6 +5,7 @@ local Foundation = require(Packages.Foundation) local React = require(Packages.React) local ReactUtils = require(Packages.ReactUtils) +local ActivityButtons = require(script.Parent.ActivityButtons) local ActivityDetails = require(script.Parent.ActivityDetails) local ActivityImage = require(script.Parent.ActivityImage) local ActivityTimer = require(script.Parent.ActivityTimer) @@ -13,9 +14,6 @@ local Types = require(script.Parent.Types) local View = Foundation.View local Text = Foundation.Text -local Button = Foundation.Button -local InputSize = Foundation.Enums.InputSize -local ButtonVariant = Foundation.Enums.ButtonVariant local createNextOrder = ReactUtils.createNextOrder @@ -29,24 +27,6 @@ type Props = { local function ActivityPreview(props: Props) local nextOrder = createNextOrder() - local buttonChildren: { React.ReactNode } = {} - if props.activity.buttons then - for index, button in props.activity.buttons do - table.insert( - buttonChildren, - e(Button, { - text = button.label, - key = `{button.label}:{index}`, - size = InputSize.Small, - variant = if index == 1 then ButtonVariant.SoftEmphasis else ButtonVariant.Standard, - width = UDim.new(1, 0), - LayoutOrder = index, - onActivated = function() end, - }) - ) - end - end - return e(View, { tag = "size-full-0 auto-y bg-surface-300 clip col padding-small gap-medium radius-small stroke-emphasis stroke-thick", LayoutOrder = props.LayoutOrder, @@ -90,10 +70,10 @@ local function ActivityPreview(props: Props) }), }), - ActivityButtons = props.activity.buttons and e(View, { - tag = "size-full-0 auto-y col gap-small", + ActivityButtons = props.activity.buttons and e(ActivityButtons, { + buttons = props.activity.buttons, LayoutOrder = nextOrder(), - }, buttonChildren), + }), }) end From 9b1586882a273c81bdaba8d1d03e6fa94aacb754 Mon Sep 17 00:00:00 2001 From: Brooke Rhodes Date: Tue, 19 May 2026 16:30:08 -0400 Subject: [PATCH 5/7] pretty up the custom profile ui --- plugin/src/Common/Constants.luau | 8 + plugin/src/CustomProfile/ActivityFields.luau | 216 +++++++++++++----- plugin/src/CustomProfile/CustomProfile.luau | 7 +- .../createCustomProfileStore.luau | 2 + plugin/src/Plugin/LocalStorageStore.luau | 4 +- .../ProfileStore/buildCustomActivity.luau | 29 ++- .../PresenceManager/ProfileStore/presets.luau | 10 +- 7 files changed, 203 insertions(+), 73 deletions(-) create mode 100644 plugin/src/Common/Constants.luau diff --git a/plugin/src/Common/Constants.luau b/plugin/src/Common/Constants.luau new file mode 100644 index 0000000..bfad082 --- /dev/null +++ b/plugin/src/Common/Constants.luau @@ -0,0 +1,8 @@ +local Constants = {} + +local REDIRECT_BASE = + "https://activity.brooke.sh/?utm_source=discord&utm_medium=rich_presence&utm_campaign=studio_activity" +Constants.PLUGIN_LINK_PROFILE_LINK = `{REDIRECT_BASE}&utm_content=profile_link` +Constants.PLUGIN_LINK_PROFILE_BUTTON = `{REDIRECT_BASE}&utm_content=profile_button` + +return table.freeze(Constants) diff --git a/plugin/src/CustomProfile/ActivityFields.luau b/plugin/src/CustomProfile/ActivityFields.luau index 55f3288..52cb66a 100644 --- a/plugin/src/CustomProfile/ActivityFields.luau +++ b/plugin/src/CustomProfile/ActivityFields.luau @@ -8,15 +8,14 @@ local ReactUtils = require(Packages.ReactUtils) local Sift = require(Packages.Sift) local ProfileContext = require(script.Parent.ProfileContext) -local Section = require(Plugin.Source.Common.Section) local View = Foundation.View -local Checkbox = Foundation.Checkbox -local TextInput = Foundation.TextInput +local List = Foundation.List local InputSize = Foundation.Enums.InputSize +local IconName = Foundation.Enums.IconName +local ListItemInputType = Foundation.Enums.ListItemInputType local createNextOrder = ReactUtils.createNextOrder -local useSignalBinding = ReactCharm.useSignalBinding local useSignalState = ReactCharm.useSignalState local e = React.createElement @@ -32,78 +31,175 @@ local function ActivityFields(props: Props) local profileStore = ProfileContext.useProfile().profileStore local currentProfile = useSignalState(profileStore.getProfile) - local profileBinding = useSignalBinding(profileStore.getProfile) + -- local profileBinding = useSignalBinding(profileStore.getProfile) + local showPlaceIcon = if currentProfile.activity.showPlaceIcon == nil then true else currentProfile.activity.showPlaceIcon + local showJoinButton = if currentProfile.activity.showJoinButton == nil + then true + else currentProfile.activity.showJoinButton + local showPluginButton = if currentProfile.activity.showPluginButton == nil + then false + else currentProfile.activity.showPluginButton - return e(Section, { - title = "Activity Fields", - canToggleExpand = true, - hasDivider = true, + return e(View, { + tag = "size-full-0 auto-y", LayoutOrder = props.LayoutOrder, }, { - LayoutBlock = e(View, { - tag = "size-full-0 auto-y col gap-medium", - LayoutOrder = nextOrder(), + List = e(List.Root, { + hasDivider = true, + isContained = { isContained = false :: false, hasMargin = false }, + size = InputSize.Small, }, { - GameIconToggle = e(Checkbox, { - label = "Show game icon", - isChecked = showPlaceIcon, - size = InputSize.Small, + ActivityText = React.createElement(List.Item, { + leading = IconName.TextUppercaseALowercaseA, + title = "Custom text", + description = "Edit the text shown on your activity.", + onActivated = function() end, + LayoutOrder = nextOrder(), + }), + + GameOverride = React.createElement(List.Item, { + leading = IconName.ControllerWithCog, + title = "Game override", + description = "Override the game displayed on your activity.", + onActivated = function() end, LayoutOrder = nextOrder(), - onActivated = function() - profileStore.setProfile(function(prev) - -- Read from `prev` so concurrent updates don't desync; treat - -- the absence of an explicit value as the default (true). - local current = if prev.activity.showPlaceIcon == nil then true else prev.activity.showPlaceIcon - return Sift.Dictionary.mergeDeep(prev, { - activity = { - showPlaceIcon = not current, - }, - }) - end) - end, }), - Details = e(TextInput, { - text = profileBinding:map(function(profile: CustomProfileData) - return profile.activity.details or "" - end), - label = "Line 1", - size = InputSize.Small, - width = UDim.new(1, 0), + GameIconToggle = React.createElement(List.Item, { + leading = IconName.FrameCamera, + title = "Show game icon", + description = "Show the platform icon of the place you're currently editing.", + onActivated = { + onActivated = function() + profileStore.setProfile(function(prev) + -- Read from `prev` so concurrent updates don't desync; treat + -- the absence of an explicit value as the default (true). + local current = if prev.activity.showPlaceIcon == nil + then true + else prev.activity.showPlaceIcon + return Sift.Dictionary.mergeDeep(prev, { + activity = { + showPlaceIcon = not current, + }, + }) + end) + end, + inputType = ListItemInputType.Toggle, + isChecked = showPlaceIcon, + }, LayoutOrder = nextOrder(), - onChanged = function(text: string) - profileStore.setProfile(function(prev) - return Sift.Dictionary.mergeDeep(prev, { - activity = { - details = if text == "" then Sift.None else text, - }, - }) - end) - end, }), - State = e(TextInput, { - text = profileBinding:map(function(profile: CustomProfileData) - return profile.activity.state or "" - end), - label = "Line 2", - size = InputSize.Small, - width = UDim.new(1, 0), + JoinButtonToggle = React.createElement(List.Item, { + leading = IconName.ChainLink, + title = "Show play button", + description = "Link the place you're currently editing with a 'Play on Roblox' button.", + onActivated = { + onActivated = function() + profileStore.setProfile(function(prev) + -- Read from `prev` so concurrent updates don't desync; treat + -- the absence of an explicit value as the default (true). + local current = if prev.activity.showJoinButton == nil + then true + else prev.activity.showJoinButton + return Sift.Dictionary.mergeDeep(prev, { + activity = { + showJoinButton = not current, + }, + }) + end) + end, + inputType = ListItemInputType.Toggle, + isChecked = showJoinButton, + }, + LayoutOrder = nextOrder(), + }), + + PluginButtonToggle = React.createElement(List.Item, { + leading = IconName.Heart, + title = "Show plugin button", + description = "Support Studio Activity by linking it on your profile.", + onActivated = { + onActivated = function() + profileStore.setProfile(function(prev) + -- Read from `prev` so concurrent updates don't desync; treat + -- the absence of an explicit value as the default (true). + local current = if prev.activity.showPluginButton == nil + then false + else prev.activity.showPluginButton + return Sift.Dictionary.mergeDeep(prev, { + activity = { + showPluginButton = not current, + }, + }) + end) + end, + inputType = ListItemInputType.Toggle, + isChecked = showPluginButton, + }, LayoutOrder = nextOrder(), - onChanged = function(text: string) - profileStore.setProfile(function(prev) - return Sift.Dictionary.mergeDeep(prev, { - activity = { - state = if text == "" then Sift.None else text, - }, - }) - end) - end, }), }), + + -- GameIconToggle = e(Checkbox, { + -- label = "Show game icon", + -- isChecked = showPlaceIcon, + -- size = InputSize.Small, + -- LayoutOrder = nextOrder(), + -- onActivated = function() + -- profileStore.setProfile(function(prev) + -- -- Read from `prev` so concurrent updates don't desync; treat + -- -- the absence of an explicit value as the default (true). + -- local current = if prev.activity.showPlaceIcon == nil then true else prev.activity.showPlaceIcon + -- return Sift.Dictionary.mergeDeep(prev, { + -- activity = { + -- showPlaceIcon = not current, + -- }, + -- }) + -- end) + -- end, + -- }), + + -- Details = e(TextInput, { + -- text = profileBinding:map(function(profile: CustomProfileData) + -- return profile.activity.details or "" + -- end), + -- label = "Line 1", + -- size = InputSize.Small, + -- width = UDim.new(1, 0), + -- LayoutOrder = nextOrder(), + -- onChanged = function(text: string) + -- profileStore.setProfile(function(prev) + -- return Sift.Dictionary.mergeDeep(prev, { + -- activity = { + -- details = if text == "" then Sift.None else text, + -- }, + -- }) + -- end) + -- end, + -- }), + + -- State = e(TextInput, { + -- text = profileBinding:map(function(profile: CustomProfileData) + -- return profile.activity.state or "" + -- end), + -- label = "Line 2", + -- size = InputSize.Small, + -- width = UDim.new(1, 0), + -- LayoutOrder = nextOrder(), + -- onChanged = function(text: string) + -- profileStore.setProfile(function(prev) + -- return Sift.Dictionary.mergeDeep(prev, { + -- activity = { + -- state = if text == "" then Sift.None else text, + -- }, + -- }) + -- end) + -- end, + -- }), }) end diff --git a/plugin/src/CustomProfile/CustomProfile.luau b/plugin/src/CustomProfile/CustomProfile.luau index 69f5a5e..2cd75b1 100644 --- a/plugin/src/CustomProfile/CustomProfile.luau +++ b/plugin/src/CustomProfile/CustomProfile.luau @@ -6,7 +6,6 @@ local React = require(Packages.React) local ReactUtils = require(Packages.ReactUtils) local ActivityFields = require(script.Parent.ActivityFields) -local GameOverride = require(script.Parent.GameOverride) local ModalStore = require(Plugin.Source.Modal.ModalStore) local PreviewBlock = require(script.Parent.PreviewBlock) local ProfileActions = require(script.Parent.ProfileActions) @@ -91,7 +90,7 @@ local function CustomProfileSheet(props: SheetProps) Content = e(Sheet.Content, nil, { Container = e(View, { - tag = "size-full-0 auto-y col padding-y-small", + tag = "size-full-0 auto-y col padding-top-small", LayoutOrder = nextOrder(), }, { PreviewBlock = e(PreviewBlock, { @@ -105,10 +104,6 @@ local function CustomProfileSheet(props: SheetProps) ActivityFields = e(ActivityFields, { LayoutOrder = nextOrder(), }), - - GameOverride = e(GameOverride, { - LayoutOrder = nextOrder(), - }), }), }), diff --git a/plugin/src/CustomProfile/createCustomProfileStore.luau b/plugin/src/CustomProfile/createCustomProfileStore.luau index 7f4682d..fe5b509 100644 --- a/plugin/src/CustomProfile/createCustomProfileStore.luau +++ b/plugin/src/CustomProfile/createCustomProfileStore.luau @@ -25,6 +25,8 @@ local DEFAULT_CUSTOM_PROFILE: CustomProfileData = { details = "Building my game", state = "{placeName}", showPlaceIcon = true, + showJoinButton = true, + showPluginButton = false, }, } diff --git a/plugin/src/Plugin/LocalStorageStore.luau b/plugin/src/Plugin/LocalStorageStore.luau index f51d266..105f118 100644 --- a/plugin/src/Plugin/LocalStorageStore.luau +++ b/plugin/src/Plugin/LocalStorageStore.luau @@ -25,8 +25,10 @@ export type CustomProfileButton = { type CustomProfileActivity = { details: string?, state: string?, - buttons: { CustomProfileButton }?, showPlaceIcon: boolean?, + showJoinButton: boolean?, + showPluginButton: boolean?, + customButtons: { CustomProfileButton }?, overridePlaceId: number?, } diff --git a/plugin/src/PresenceManager/ProfileStore/buildCustomActivity.luau b/plugin/src/PresenceManager/ProfileStore/buildCustomActivity.luau index e1fd310..86529e2 100644 --- a/plugin/src/PresenceManager/ProfileStore/buildCustomActivity.luau +++ b/plugin/src/PresenceManager/ProfileStore/buildCustomActivity.luau @@ -1,5 +1,6 @@ local Plugin = script:FindFirstAncestor("StudioActivity") +local Constants = require(Plugin.Source.Common.Constants) local LocalStorageStore = require(Plugin.Source.Plugin.LocalStorageStore) local PlaceDetailsStore = require(Plugin.Source.PlaceDetailsStore) local createPresenceManager = require(Plugin.Source.PresenceManager.createPresenceManager) @@ -10,6 +11,7 @@ type CustomProfileData = LocalStorageStore.CustomProfileData type PlaceContext = PlaceDetailsStore.PlaceContext local STUDIO_LOGO_ASSET = "studio-logo" +local MAX_CUSTOM_BUTTONS = 2 local function buildCustomActivity(context: PlaceContext, profile: CustomProfileData): ActivityConfig local onlineDetails = context.onlinePlaceDetails @@ -26,12 +28,35 @@ local function buildCustomActivity(context: PlaceContext, profile: CustomProfile creator = if onlineDetails then onlineDetails.creatorName else "", } + local buttons: { createPresenceManager.ActivityConfigButton } = {} + if onlineDetails and profile.activity.showJoinButton then + table.insert(buttons, { + label = "Play on Roblox", + url = onlineDetails.placeLink, + }) + end + + if profile.activity.showPluginButton then + table.insert(buttons, { + label = "Get Studio Activity", + url = Constants.PLUGIN_LINK_PROFILE_BUTTON, + }) + end + + if profile.activity.customButtons then + local remainingButtons = MAX_CUSTOM_BUTTONS - #buttons + for i = 1, remainingButtons do + local customButton = profile.activity.customButtons[i] + table.insert(buttons, customButton) + end + end + local activity: ActivityConfig = { name = "Roblox Studio", details = if details then interpolate(details, templates) else nil, - detailsUrl = "https://github.com/grilme99/studio-activity", state = if state then interpolate(state, templates) else nil, - buttons = profile.activity.buttons, + detailsUrl = Constants.PLUGIN_LINK_PROFILE_LINK, + buttons = if #buttons > 0 then buttons else nil, assets = if profile.activity.showPlaceIcon and onlineDetails then { largeImage = onlineDetails.iconWebUrl, diff --git a/plugin/src/PresenceManager/ProfileStore/presets.luau b/plugin/src/PresenceManager/ProfileStore/presets.luau index 122a0bc..4252f42 100644 --- a/plugin/src/PresenceManager/ProfileStore/presets.luau +++ b/plugin/src/PresenceManager/ProfileStore/presets.luau @@ -3,13 +3,14 @@ local Plugin = script:FindFirstAncestor("StudioActivity") local Packages = Plugin.Packages local Foundation = require(Packages.Foundation) +local Constants = require(Plugin.Source.Common.Constants) +local PlaceDetailsStore = require(Plugin.Source.PlaceDetailsStore) +local createPresenceManager = require(Plugin.Source.PresenceManager.createPresenceManager) + local IconName = Foundation.Enums.IconName type IconName = Foundation.IconName -local createPresenceManager = require(Plugin.Source.PresenceManager.createPresenceManager) type ActivityConfig = createPresenceManager.ActivityConfig - -local PlaceDetailsStore = require(Plugin.Source.PlaceDetailsStore) type PlaceContext = PlaceDetailsStore.PlaceContext local STUDIO_LOGO_ASSET = "studio-logo" @@ -35,7 +36,7 @@ local presets: { [string]: PresetProfile } = { then onlineDetails.placeName elseif offlineDetails then offlineDetails.placeName else nil, - stateUrl = if onlineDetails then onlineDetails.placeLink else nil, + detailsUrl = Constants.PLUGIN_LINK_PROFILE_LINK, buttons = if onlineDetails then { { label = "Play on Roblox", url = onlineDetails.placeLink }, @@ -62,6 +63,7 @@ local presets: { [string]: PresetProfile } = { return { name = "Roblox Studio", details = "Editing in Studio", + detailsUrl = Constants.PLUGIN_LINK_PROFILE_LINK, assets = { largeImage = STUDIO_LOGO_ASSET, largeText = "Studio Activity", From 693f1ca5ad5894b8d0f3d8221962300f2653ea74 Mon Sep 17 00:00:00 2001 From: Brooke Rhodes Date: Tue, 19 May 2026 20:46:51 -0400 Subject: [PATCH 6/7] hook up custom text and game override --- plugin/src/CustomProfile/ActivityFields.luau | 21 +++- .../CustomProfileGameOverride.luau | 104 ++++++++++++++++ .../OverridePreview.luau | 6 +- .../CustomProfileGameOverride/init.luau | 1 + .../CustomProfileText/CustomProfileText.luau | 115 ++++++++++++++++++ .../CustomProfileText/TemplateHint.luau | 42 +++++++ .../CustomProfile/CustomProfileText/init.luau | 1 + .../GameOverride/GameOverride.luau | 79 ------------ .../src/CustomProfile/GameOverride/init.luau | 1 - plugin/src/Modal/ConfirmationDialog.luau | 1 - plugin/src/Modal/ModalRenderer.luau | 25 ++++ .../Modal/ModalStore/createModalStore.luau | 9 +- 12 files changed, 318 insertions(+), 87 deletions(-) create mode 100644 plugin/src/CustomProfile/CustomProfileGameOverride/CustomProfileGameOverride.luau rename plugin/src/CustomProfile/{GameOverride => CustomProfileGameOverride}/OverridePreview.luau (95%) create mode 100644 plugin/src/CustomProfile/CustomProfileGameOverride/init.luau create mode 100644 plugin/src/CustomProfile/CustomProfileText/CustomProfileText.luau create mode 100644 plugin/src/CustomProfile/CustomProfileText/TemplateHint.luau create mode 100644 plugin/src/CustomProfile/CustomProfileText/init.luau delete mode 100644 plugin/src/CustomProfile/GameOverride/GameOverride.luau delete mode 100644 plugin/src/CustomProfile/GameOverride/init.luau diff --git a/plugin/src/CustomProfile/ActivityFields.luau b/plugin/src/CustomProfile/ActivityFields.luau index 52cb66a..3692dc0 100644 --- a/plugin/src/CustomProfile/ActivityFields.luau +++ b/plugin/src/CustomProfile/ActivityFields.luau @@ -7,6 +7,7 @@ local ReactCharm = require(Packages.ReactCharm) local ReactUtils = require(Packages.ReactUtils) local Sift = require(Packages.Sift) +local ModalStore = require(Plugin.Source.Modal.ModalStore) local ProfileContext = require(script.Parent.ProfileContext) local View = Foundation.View @@ -16,6 +17,7 @@ local IconName = Foundation.Enums.IconName local ListItemInputType = Foundation.Enums.ListItemInputType local createNextOrder = ReactUtils.createNextOrder +local useSignalBinding = ReactCharm.useSignalBinding local useSignalState = ReactCharm.useSignalState local e = React.createElement @@ -31,7 +33,7 @@ local function ActivityFields(props: Props) local profileStore = ProfileContext.useProfile().profileStore local currentProfile = useSignalState(profileStore.getProfile) - -- local profileBinding = useSignalBinding(profileStore.getProfile) + local profileBinding = useSignalBinding(profileStore.getProfile) local showPlaceIcon = if currentProfile.activity.showPlaceIcon == nil then true @@ -56,7 +58,14 @@ local function ActivityFields(props: Props) leading = IconName.TextUppercaseALowercaseA, title = "Custom text", description = "Edit the text shown on your activity.", - onActivated = function() end, + onActivated = function() + ModalStore.get().open("customProfileText", { + props = { + profile = profileBinding, + store = profileStore, + }, + }) + end, LayoutOrder = nextOrder(), }), @@ -64,7 +73,13 @@ local function ActivityFields(props: Props) leading = IconName.ControllerWithCog, title = "Game override", description = "Override the game displayed on your activity.", - onActivated = function() end, + onActivated = function() + ModalStore.get().open("customProfileGameOverride", { + props = { + store = profileStore, + }, + }) + end, LayoutOrder = nextOrder(), }), diff --git a/plugin/src/CustomProfile/CustomProfileGameOverride/CustomProfileGameOverride.luau b/plugin/src/CustomProfile/CustomProfileGameOverride/CustomProfileGameOverride.luau new file mode 100644 index 0000000..d3babd8 --- /dev/null +++ b/plugin/src/CustomProfile/CustomProfileGameOverride/CustomProfileGameOverride.luau @@ -0,0 +1,104 @@ +local Plugin = script:FindFirstAncestor("StudioActivity") + +local Packages = Plugin.Packages +local Foundation = require(Packages.Foundation) +local React = require(Packages.React) +local ReactCharm = require(Packages.ReactCharm) +local ReactUtils = require(Packages.ReactUtils) + +local OverridePreview = require(script.Parent.OverridePreview) +local ProfileContext = require(script.Parent.Parent.ProfileContext) + +local View = Foundation.View +local Dialog = Foundation.Dialog +local NumberInput = Foundation.NumberInput +local DialogSize = Foundation.Enums.DialogSize +local InputSize = Foundation.Enums.InputSize +local ButtonVariant = Foundation.Enums.ButtonVariant +local OnChangeCallbackReason = Foundation.Enums.OnChangeCallbackReason +local NumberInputControlsVariant = Foundation.Enums.NumberInputControlsVariant + +type OnChangeCallbackReason = Foundation.OnChangeCallbackReason +type CustomProfileStore = ProfileContext.CustomProfileStore + +local createNextOrder = ReactUtils.createNextOrder +local useSignalState = ReactCharm.useSignalState + +local e = React.createElement + +type Props = { + store: CustomProfileStore, + onClose: () -> (), +} + +local function CustomProfileGameOverride(props: Props) + local nextOrder = createNextOrder() + + local override = useSignalState(props.store.getOverrideLookupState) + + return e(Dialog.Root, { + size = DialogSize.Medium, + hasBackdrop = true, + disablePortal = false, + onClose = function() + props.onClose() + end, + }, { + DialogTitle = e(Dialog.Title, { + text = "Game Override", + }), + + DialogContent = e(Dialog.Content, nil, { + LayoutBlock = e(View, { + tag = "size-full-0 auto-y col gap-medium padding-xsmall", + LayoutOrder = nextOrder(), + }, { + PlaceIdInput = e(NumberInput, { + label = "Place ID", + hint = "Optionally override the game displayed on your activity. If unset, the place you're currently editing will be used instead.", + value = override.placeId or 0, + onChanged = function(placeId: number, reason: OnChangeCallbackReason) + -- NumberInput re-fires onChanged with its `value` prop on focus loss. + -- While our lookup debounce is in flight `value` collapses to 0, + -- which would normalise to "" and wipe the override. Keyboard + -- events already capture every real edit, so we can safely + -- ignore FocusLost. + if reason == OnChangeCallbackReason.FocusLost then + return + end + props.store.setOverrideLookupInput(if placeId == 0 then "" else tostring(placeId)) + end, + formatAsString = function(placeId: number) + return if override.status == "loading" + then "Loading..." + elseif placeId == 0 then "None" + else tostring(placeId) + end, + controlsVariant = NumberInputControlsVariant.None, + size = InputSize.Small, + width = UDim.new(1, 0), + LayoutOrder = nextOrder(), + }), + + OverridePreview = e(OverridePreview, { + store = props.store, + LayoutOrder = nextOrder(), + }), + }), + }), + + DialogActions = e(Dialog.Actions, { + actions = { + { + text = "Save", + variant = ButtonVariant.Emphasis, + onActivated = function() + props.onClose() + end, + }, + }, + }), + }) +end + +return CustomProfileGameOverride diff --git a/plugin/src/CustomProfile/GameOverride/OverridePreview.luau b/plugin/src/CustomProfile/CustomProfileGameOverride/OverridePreview.luau similarity index 95% rename from plugin/src/CustomProfile/GameOverride/OverridePreview.luau rename to plugin/src/CustomProfile/CustomProfileGameOverride/OverridePreview.luau index 7f2e262..cc83374 100644 --- a/plugin/src/CustomProfile/GameOverride/OverridePreview.luau +++ b/plugin/src/CustomProfile/CustomProfileGameOverride/OverridePreview.luau @@ -20,17 +20,19 @@ local Radius = Foundation.Enums.Radius local createNextOrder = ReactUtils.createNextOrder local useSignalState = ReactCharm.useSignalState +type CustomProfileStore = ProfileContext.CustomProfileStore + local e = React.createElement type Props = { + store: CustomProfileStore, LayoutOrder: number?, } local function OverridePreview(props: Props) local nextOrder = createNextOrder() - local profile = ProfileContext.useProfile() - local override = useSignalState(profile.profileStore.getOverrideLookupState) + local override = useSignalState(props.store.getOverrideLookupState) local placeDetails = override.context and override.context.onlinePlaceDetails local iconWebUrl = placeDetails and placeDetails.iconWebUrl diff --git a/plugin/src/CustomProfile/CustomProfileGameOverride/init.luau b/plugin/src/CustomProfile/CustomProfileGameOverride/init.luau new file mode 100644 index 0000000..0eb9500 --- /dev/null +++ b/plugin/src/CustomProfile/CustomProfileGameOverride/init.luau @@ -0,0 +1 @@ +return require(script.CustomProfileGameOverride) diff --git a/plugin/src/CustomProfile/CustomProfileText/CustomProfileText.luau b/plugin/src/CustomProfile/CustomProfileText/CustomProfileText.luau new file mode 100644 index 0000000..e8ca170 --- /dev/null +++ b/plugin/src/CustomProfile/CustomProfileText/CustomProfileText.luau @@ -0,0 +1,115 @@ +local Plugin = script:FindFirstAncestor("StudioActivity") + +local Packages = Plugin.Packages +local Foundation = require(Packages.Foundation) +local React = require(Packages.React) +local ReactUtils = require(Packages.ReactUtils) +local Sift = require(Packages.Sift) + +local ProfileContext = require(script.Parent.Parent.ProfileContext) +local TemplateHint = require(script.Parent.TemplateHint) + +local View = Foundation.View +local Dialog = Foundation.Dialog +local TextInput = Foundation.TextInput +local DialogSize = Foundation.Enums.DialogSize +local ButtonVariant = Foundation.Enums.ButtonVariant +local InputSize = Foundation.Enums.InputSize + +local createNextOrder = ReactUtils.createNextOrder + +local e = React.createElement + +type CustomProfileData = ProfileContext.CustomProfileData +type CustomProfileStore = ProfileContext.CustomProfileStore + +type Props = { + profile: React.Binding, + store: CustomProfileStore, + onClose: () -> (), +} + +local function CustomProfileText(props: Props) + local nextOrder = createNextOrder() + + return e(Dialog.Root, { + size = DialogSize.Medium, + hasBackdrop = true, + disablePortal = false, + onClose = function() + props.onClose() + end, + }, { + DialogTitle = e(Dialog.Title, { + text = "Custom Text", + }), + + DialogContent = e(Dialog.Content, nil, { + Container = e(View, { + tag = "size-full-0 auto-y col gap-large padding-xsmall padding-top-small", + LayoutOrder = nextOrder(), + }, { + InputContainer = e(View, { + tag = "size-full-0 auto-y col gap-medium", + LayoutOrder = nextOrder(), + }, { + Details = e(TextInput, { + text = props.profile:map(function(profile: CustomProfileData) + return profile.activity.details or "" + end), + label = "Line 1", + size = InputSize.Small, + width = UDim.new(1, 0), + LayoutOrder = nextOrder(), + onChanged = function(text: string) + props.store.setProfile(function(prev) + return Sift.Dictionary.mergeDeep(prev, { + activity = { + details = if text == "" then Sift.None else text, + }, + }) + end) + end, + }), + + State = e(TextInput, { + text = props.profile:map(function(profile: CustomProfileData) + return profile.activity.state or "" + end), + label = "Line 2", + size = InputSize.Small, + width = UDim.new(1, 0), + LayoutOrder = nextOrder(), + onChanged = function(text: string) + props.store.setProfile(function(prev) + return Sift.Dictionary.mergeDeep(prev, { + activity = { + state = if text == "" then Sift.None else text, + }, + }) + end) + end, + }), + }), + + TemplateHint = e(TemplateHint, { + LayoutOrder = nextOrder(), + }), + }), + }), + + DialogActions = e(Dialog.Actions, { + actions = { + { + text = "Save", + variant = ButtonVariant.Emphasis, + onActivated = function() + props.onClose() + end, + }, + }, + }), + }) +end + +return React.memo(CustomProfileText) diff --git a/plugin/src/CustomProfile/CustomProfileText/TemplateHint.luau b/plugin/src/CustomProfile/CustomProfileText/TemplateHint.luau new file mode 100644 index 0000000..b1aa8b7 --- /dev/null +++ b/plugin/src/CustomProfile/CustomProfileText/TemplateHint.luau @@ -0,0 +1,42 @@ +local Plugin = script:FindFirstAncestor("StudioActivity") + +local Packages = Plugin.Packages +local Foundation = require(Packages.Foundation) +local React = require(Packages.React) + +local FeedbackAlert = Foundation.FeedbackAlert +local AlertSeverity = Foundation.Enums.AlertSeverity + +local e = React.createElement + +local TEMPLATES = { + "placeName", + "creator", +} + +type Props = { + LayoutOrder: number?, +} + +local function TemplateHint(props: Props) + local description = + "Use the templating syntax to add dynamic data to your Discord activity. Available templates:\n\n" + + for i, template in TEMPLATES do + description ..= `• \{{template}\}` + if i ~= #TEMPLATES then + description ..= "\n" + end + end + + description ..= "\n\nExample: I'm working on {placeName}" + + return e(FeedbackAlert, { + severity = AlertSeverity.Info, + title = "Templating Syntax", + description = description, + LayoutOrder = props.LayoutOrder, + }, {}) +end + +return TemplateHint diff --git a/plugin/src/CustomProfile/CustomProfileText/init.luau b/plugin/src/CustomProfile/CustomProfileText/init.luau new file mode 100644 index 0000000..3518297 --- /dev/null +++ b/plugin/src/CustomProfile/CustomProfileText/init.luau @@ -0,0 +1 @@ +return require(script.CustomProfileText) diff --git a/plugin/src/CustomProfile/GameOverride/GameOverride.luau b/plugin/src/CustomProfile/GameOverride/GameOverride.luau deleted file mode 100644 index 8b27ba8..0000000 --- a/plugin/src/CustomProfile/GameOverride/GameOverride.luau +++ /dev/null @@ -1,79 +0,0 @@ -local Plugin = script:FindFirstAncestor("StudioActivity") - -local Packages = Plugin.Packages -local Foundation = require(Packages.Foundation) -local React = require(Packages.React) -local ReactCharm = require(Packages.ReactCharm) -local ReactUtils = require(Packages.ReactUtils) - -local OverridePreview = require(script.Parent.OverridePreview) -local ProfileContext = require(script.Parent.Parent.ProfileContext) -local Section = require(Plugin.Source.Common.Section) - -local View = Foundation.View -local NumberInput = Foundation.NumberInput -local InputSize = Foundation.Enums.InputSize -local NumberInputControlsVariant = Foundation.Enums.NumberInputControlsVariant -local OnChangeCallbackReason = Foundation.Enums.OnChangeCallbackReason -type OnChangeCallbackReason = Foundation.OnChangeCallbackReason - -local createNextOrder = ReactUtils.createNextOrder -local useSignalState = ReactCharm.useSignalState - -local e = React.createElement - -type Props = { - LayoutOrder: number?, -} - -local function GameOverride(props: Props) - local nextOrder = createNextOrder() - local profile = ProfileContext.useProfile() - - local override = useSignalState(profile.profileStore.getOverrideLookupState) - - return e(Section, { - title = "Game Override", - canToggleExpand = true, - hasDivider = false, - LayoutOrder = props.LayoutOrder, - }, { - LayoutBlock = e(View, { - tag = "size-full-0 auto-y col gap-medium", - LayoutOrder = nextOrder(), - }, { - PlaceIdInput = e(NumberInput, { - label = "Place ID", - hint = "Optionally override the game displayed on your activity. If unset, the place you're currently editing will be used instead.", - value = override.placeId or 0, - onChanged = function(placeId: number, reason: OnChangeCallbackReason) - -- NumberInput re-fires onChanged with its `value` prop on focus loss. - -- While our lookup debounce is in flight `value` collapses to 0, - -- which would normalise to "" and wipe the override. Keyboard - -- events already capture every real edit, so we can safely - -- ignore FocusLost. - if reason == OnChangeCallbackReason.FocusLost then - return - end - profile.profileStore.setOverrideLookupInput(if placeId == 0 then "" else tostring(placeId)) - end, - formatAsString = function(placeId: number) - return if override.status == "loading" - then "Loading..." - elseif placeId == 0 then "None" - else tostring(placeId) - end, - controlsVariant = NumberInputControlsVariant.None, - size = InputSize.Small, - width = UDim.new(1, 0), - LayoutOrder = nextOrder(), - }), - - OverridePreview = e(OverridePreview, { - LayoutOrder = nextOrder(), - }), - }), - }) -end - -return React.memo(GameOverride) diff --git a/plugin/src/CustomProfile/GameOverride/init.luau b/plugin/src/CustomProfile/GameOverride/init.luau deleted file mode 100644 index 9d5396e..0000000 --- a/plugin/src/CustomProfile/GameOverride/init.luau +++ /dev/null @@ -1 +0,0 @@ -return require(script.GameOverride) diff --git a/plugin/src/Modal/ConfirmationDialog.luau b/plugin/src/Modal/ConfirmationDialog.luau index a4c6563..2ea30c8 100644 --- a/plugin/src/Modal/ConfirmationDialog.luau +++ b/plugin/src/Modal/ConfirmationDialog.luau @@ -14,7 +14,6 @@ type Props = { title: string?, body: string?, onClose: (result: boolean) -> (), - LayoutOrder: number?, } local function ConfirmationDialog(props: Props) diff --git a/plugin/src/Modal/ModalRenderer.luau b/plugin/src/Modal/ModalRenderer.luau index 32b191a..2e8c6b6 100644 --- a/plugin/src/Modal/ModalRenderer.luau +++ b/plugin/src/Modal/ModalRenderer.luau @@ -7,6 +7,8 @@ local ReactCharm = require(Packages.ReactCharm) local AddAccountFlow = require(Plugin.Source.AddAccountFlow) local ConfirmationDialog = require(script.Parent.ConfirmationDialog) local CustomProfile = require(Plugin.Source.CustomProfile) +local CustomProfileGameOverride = require(Plugin.Source.CustomProfile.CustomProfileGameOverride) +local CustomProfileText = require(Plugin.Source.CustomProfile.CustomProfileText) local ModalStore = require(script.Parent.ModalStore) local SettingsModal = require(Plugin.Source.UserSettings.SettingsModal) local WelcomeDialog = require(Plugin.Source.Telemetry.TelemetryOptOutDialog) @@ -71,6 +73,29 @@ local MODAL_REGISTRY: { [ModalStore.ModalId]: ModalConfig } = { }) end, }, + + customProfileText = { + type = "dialog", + render = function(isOpen, onClose, props) + return e(CustomProfileText, { + isOpen = isOpen, + onClose = onClose, + profile = props.profile, + store = props.store, + }) + end, + }, + + customProfileGameOverride = { + type = "dialog", + render = function(isOpen, onClose, props) + return e(CustomProfileGameOverride, { + isOpen = isOpen, + onClose = onClose, + store = props.store, + }) + end, + }, } local function ModalRenderer() diff --git a/plugin/src/Modal/ModalStore/createModalStore.luau b/plugin/src/Modal/ModalStore/createModalStore.luau index 5dadf4c..48d7907 100644 --- a/plugin/src/Modal/ModalStore/createModalStore.luau +++ b/plugin/src/Modal/ModalStore/createModalStore.luau @@ -6,7 +6,14 @@ local Charm = require(Packages.Charm) local signal = Charm.signal local computed = Charm.computed -export type ModalId = "addAccount" | "welcome" | "confirmation" | "settings" | "customProfile" +export type ModalId = + "addAccount" + | "welcome" + | "confirmation" + | "settings" + | "customProfile" + | "customProfileText" + | "customProfileGameOverride" export type ModalStackEntry = { id: ModalId, From aecd3f250e1c5b97039dacfad6dbe6dbc08a414a Mon Sep 17 00:00:00 2001 From: Brooke Rhodes Date: Tue, 19 May 2026 21:12:02 -0400 Subject: [PATCH 7/7] feedback --- backend/src/routes/gh_redirect.rs | 2 +- .../src/ActivityPreview/ActivityButtons.luau | 6 +- .../src/ActivityPreview/ActivityPreview.luau | 2 +- plugin/src/CustomProfile/CustomProfile.luau | 4 +- .../CustomProfileGameOverride.luau | 2 +- ...{ActivityFields.luau => SettingsList.luau} | 105 +++--------------- plugin/src/Modal/ModalRenderer.luau | 6 +- .../ProfileStore/buildCustomActivity.luau | 8 +- 8 files changed, 32 insertions(+), 103 deletions(-) rename plugin/src/CustomProfile/{ActivityFields.luau => SettingsList.luau} (52%) diff --git a/backend/src/routes/gh_redirect.rs b/backend/src/routes/gh_redirect.rs index e295a99..796baa0 100644 --- a/backend/src/routes/gh_redirect.rs +++ b/backend/src/routes/gh_redirect.rs @@ -7,7 +7,7 @@ use crate::extractors::{Edge, WorkerContext}; use crate::posthog; const DEFAULT_POSTHOG_HOST: &str = "https://us.i.posthog.com"; -const GITHUB_REPO_URL: &str = "https://github.com/grilme99/studio-activity"; +const GITHUB_REPO_URL: &str = "https://github.com/BrookenRecord/studio-activity"; #[allow(clippy::must_use_candidate)] #[tracing::instrument( diff --git a/plugin/src/ActivityPreview/ActivityButtons.luau b/plugin/src/ActivityPreview/ActivityButtons.luau index f91d0da..ac6f952 100644 --- a/plugin/src/ActivityPreview/ActivityButtons.luau +++ b/plugin/src/ActivityPreview/ActivityButtons.luau @@ -20,6 +20,8 @@ local createNextOrder = ReactUtils.createNextOrder local e = React.createElement +local function NOOP() end + type Props = { buttons: { Types.ActivityButton }, LayoutOrder: number?, @@ -34,13 +36,13 @@ local function ActivityButtons(props: Props) buttonChildren, e(Button, { text = button.label, - key = `{button.label}:{index}`, + key = tostring(index), size = InputSize.Small, variant = if index == 1 then ButtonVariant.SoftEmphasis else ButtonVariant.Standard, fillBehavior = FillBehavior.Fill, width = UDim.new(1, 0), LayoutOrder = index, - onActivated = function() end, + onActivated = NOOP, }) ) end diff --git a/plugin/src/ActivityPreview/ActivityPreview.luau b/plugin/src/ActivityPreview/ActivityPreview.luau index debae47..1a22b68 100644 --- a/plugin/src/ActivityPreview/ActivityPreview.luau +++ b/plugin/src/ActivityPreview/ActivityPreview.luau @@ -70,7 +70,7 @@ local function ActivityPreview(props: Props) }), }), - ActivityButtons = props.activity.buttons and e(ActivityButtons, { + ActivityButtons = (props.activity.buttons and #props.activity.buttons > 0) and e(ActivityButtons, { buttons = props.activity.buttons, LayoutOrder = nextOrder(), }), diff --git a/plugin/src/CustomProfile/CustomProfile.luau b/plugin/src/CustomProfile/CustomProfile.luau index 2cd75b1..36c27f3 100644 --- a/plugin/src/CustomProfile/CustomProfile.luau +++ b/plugin/src/CustomProfile/CustomProfile.luau @@ -5,12 +5,12 @@ local Foundation = require(Packages.Foundation) local React = require(Packages.React) local ReactUtils = require(Packages.ReactUtils) -local ActivityFields = require(script.Parent.ActivityFields) local ModalStore = require(Plugin.Source.Modal.ModalStore) local PreviewBlock = require(script.Parent.PreviewBlock) local ProfileActions = require(script.Parent.ProfileActions) local ProfileContext = require(script.Parent.ProfileContext) local ProfileName = require(script.Parent.ProfileName) +local SettingsList = require(script.Parent.SettingsList) local Title = require(script.Parent.Title) local View = Foundation.View @@ -101,7 +101,7 @@ local function CustomProfileSheet(props: SheetProps) LayoutOrder = nextOrder(), }), - ActivityFields = e(ActivityFields, { + SettingsList = e(SettingsList, { LayoutOrder = nextOrder(), }), }), diff --git a/plugin/src/CustomProfile/CustomProfileGameOverride/CustomProfileGameOverride.luau b/plugin/src/CustomProfile/CustomProfileGameOverride/CustomProfileGameOverride.luau index d3babd8..70fa64d 100644 --- a/plugin/src/CustomProfile/CustomProfileGameOverride/CustomProfileGameOverride.luau +++ b/plugin/src/CustomProfile/CustomProfileGameOverride/CustomProfileGameOverride.luau @@ -101,4 +101,4 @@ local function CustomProfileGameOverride(props: Props) }) end -return CustomProfileGameOverride +return React.memo(CustomProfileGameOverride) diff --git a/plugin/src/CustomProfile/ActivityFields.luau b/plugin/src/CustomProfile/SettingsList.luau similarity index 52% rename from plugin/src/CustomProfile/ActivityFields.luau rename to plugin/src/CustomProfile/SettingsList.luau index 3692dc0..64fb654 100644 --- a/plugin/src/CustomProfile/ActivityFields.luau +++ b/plugin/src/CustomProfile/SettingsList.luau @@ -24,26 +24,24 @@ local e = React.createElement type CustomProfileData = ProfileContext.CustomProfileData +local function withDefault(value: boolean?, default: boolean): boolean + return if value == nil then default else value +end + type Props = { LayoutOrder: number?, } -local function ActivityFields(props: Props) +local function SettingsList(props: Props) local nextOrder = createNextOrder() local profileStore = ProfileContext.useProfile().profileStore local currentProfile = useSignalState(profileStore.getProfile) local profileBinding = useSignalBinding(profileStore.getProfile) - local showPlaceIcon = if currentProfile.activity.showPlaceIcon == nil - then true - else currentProfile.activity.showPlaceIcon - local showJoinButton = if currentProfile.activity.showJoinButton == nil - then true - else currentProfile.activity.showJoinButton - local showPluginButton = if currentProfile.activity.showPluginButton == nil - then false - else currentProfile.activity.showPluginButton + local showPlaceIcon = withDefault(currentProfile.activity.showPlaceIcon, true) + local showJoinButton = withDefault(currentProfile.activity.showJoinButton, true) + local showPluginButton = withDefault(currentProfile.activity.showPluginButton, false) return e(View, { tag = "size-full-0 auto-y", @@ -54,7 +52,7 @@ local function ActivityFields(props: Props) isContained = { isContained = false :: false, hasMargin = false }, size = InputSize.Small, }, { - ActivityText = React.createElement(List.Item, { + ActivityText = e(List.Item, { leading = IconName.TextUppercaseALowercaseA, title = "Custom text", description = "Edit the text shown on your activity.", @@ -69,7 +67,7 @@ local function ActivityFields(props: Props) LayoutOrder = nextOrder(), }), - GameOverride = React.createElement(List.Item, { + GameOverride = e(List.Item, { leading = IconName.ControllerWithCog, title = "Game override", description = "Override the game displayed on your activity.", @@ -83,18 +81,14 @@ local function ActivityFields(props: Props) LayoutOrder = nextOrder(), }), - GameIconToggle = React.createElement(List.Item, { + GameIconToggle = e(List.Item, { leading = IconName.FrameCamera, title = "Show game icon", description = "Show the platform icon of the place you're currently editing.", onActivated = { onActivated = function() profileStore.setProfile(function(prev) - -- Read from `prev` so concurrent updates don't desync; treat - -- the absence of an explicit value as the default (true). - local current = if prev.activity.showPlaceIcon == nil - then true - else prev.activity.showPlaceIcon + local current = withDefault(prev.activity.showPlaceIcon, true) return Sift.Dictionary.mergeDeep(prev, { activity = { showPlaceIcon = not current, @@ -108,18 +102,14 @@ local function ActivityFields(props: Props) LayoutOrder = nextOrder(), }), - JoinButtonToggle = React.createElement(List.Item, { + JoinButtonToggle = e(List.Item, { leading = IconName.ChainLink, title = "Show play button", description = "Link the place you're currently editing with a 'Play on Roblox' button.", onActivated = { onActivated = function() profileStore.setProfile(function(prev) - -- Read from `prev` so concurrent updates don't desync; treat - -- the absence of an explicit value as the default (true). - local current = if prev.activity.showJoinButton == nil - then true - else prev.activity.showJoinButton + local current = withDefault(prev.activity.showJoinButton, true) return Sift.Dictionary.mergeDeep(prev, { activity = { showJoinButton = not current, @@ -133,18 +123,14 @@ local function ActivityFields(props: Props) LayoutOrder = nextOrder(), }), - PluginButtonToggle = React.createElement(List.Item, { + PluginButtonToggle = e(List.Item, { leading = IconName.Heart, title = "Show plugin button", description = "Support Studio Activity by linking it on your profile.", onActivated = { onActivated = function() profileStore.setProfile(function(prev) - -- Read from `prev` so concurrent updates don't desync; treat - -- the absence of an explicit value as the default (true). - local current = if prev.activity.showPluginButton == nil - then false - else prev.activity.showPluginButton + local current = withDefault(prev.activity.showPluginButton, false) return Sift.Dictionary.mergeDeep(prev, { activity = { showPluginButton = not current, @@ -158,64 +144,7 @@ local function ActivityFields(props: Props) LayoutOrder = nextOrder(), }), }), - - -- GameIconToggle = e(Checkbox, { - -- label = "Show game icon", - -- isChecked = showPlaceIcon, - -- size = InputSize.Small, - -- LayoutOrder = nextOrder(), - -- onActivated = function() - -- profileStore.setProfile(function(prev) - -- -- Read from `prev` so concurrent updates don't desync; treat - -- -- the absence of an explicit value as the default (true). - -- local current = if prev.activity.showPlaceIcon == nil then true else prev.activity.showPlaceIcon - -- return Sift.Dictionary.mergeDeep(prev, { - -- activity = { - -- showPlaceIcon = not current, - -- }, - -- }) - -- end) - -- end, - -- }), - - -- Details = e(TextInput, { - -- text = profileBinding:map(function(profile: CustomProfileData) - -- return profile.activity.details or "" - -- end), - -- label = "Line 1", - -- size = InputSize.Small, - -- width = UDim.new(1, 0), - -- LayoutOrder = nextOrder(), - -- onChanged = function(text: string) - -- profileStore.setProfile(function(prev) - -- return Sift.Dictionary.mergeDeep(prev, { - -- activity = { - -- details = if text == "" then Sift.None else text, - -- }, - -- }) - -- end) - -- end, - -- }), - - -- State = e(TextInput, { - -- text = profileBinding:map(function(profile: CustomProfileData) - -- return profile.activity.state or "" - -- end), - -- label = "Line 2", - -- size = InputSize.Small, - -- width = UDim.new(1, 0), - -- LayoutOrder = nextOrder(), - -- onChanged = function(text: string) - -- profileStore.setProfile(function(prev) - -- return Sift.Dictionary.mergeDeep(prev, { - -- activity = { - -- state = if text == "" then Sift.None else text, - -- }, - -- }) - -- end) - -- end, - -- }), }) end -return React.memo(ActivityFields) +return React.memo(SettingsList) diff --git a/plugin/src/Modal/ModalRenderer.luau b/plugin/src/Modal/ModalRenderer.luau index 2e8c6b6..5e69c11 100644 --- a/plugin/src/Modal/ModalRenderer.luau +++ b/plugin/src/Modal/ModalRenderer.luau @@ -76,9 +76,8 @@ local MODAL_REGISTRY: { [ModalStore.ModalId]: ModalConfig } = { customProfileText = { type = "dialog", - render = function(isOpen, onClose, props) + render = function(_isOpen, onClose, props) return e(CustomProfileText, { - isOpen = isOpen, onClose = onClose, profile = props.profile, store = props.store, @@ -88,9 +87,8 @@ local MODAL_REGISTRY: { [ModalStore.ModalId]: ModalConfig } = { customProfileGameOverride = { type = "dialog", - render = function(isOpen, onClose, props) + render = function(_isOpen, onClose, props) return e(CustomProfileGameOverride, { - isOpen = isOpen, onClose = onClose, store = props.store, }) diff --git a/plugin/src/PresenceManager/ProfileStore/buildCustomActivity.luau b/plugin/src/PresenceManager/ProfileStore/buildCustomActivity.luau index 86529e2..b535aaf 100644 --- a/plugin/src/PresenceManager/ProfileStore/buildCustomActivity.luau +++ b/plugin/src/PresenceManager/ProfileStore/buildCustomActivity.luau @@ -44,10 +44,10 @@ local function buildCustomActivity(context: PlaceContext, profile: CustomProfile end if profile.activity.customButtons then - local remainingButtons = MAX_CUSTOM_BUTTONS - #buttons - for i = 1, remainingButtons do - local customButton = profile.activity.customButtons[i] - table.insert(buttons, customButton) + local remaining = MAX_CUSTOM_BUTTONS - #buttons + local count = math.min(remaining, #profile.activity.customButtons) + for i = 1, count do + table.insert(buttons, profile.activity.customButtons[i]) end end