Improvements to sync reminder UX (#1096)

This commit is contained in:
boatbomber
2025-08-28 17:15:34 -07:00
committed by GitHub
parent 4e9e6233ff
commit 54e0ff230b
7 changed files with 513 additions and 137 deletions

View File

@@ -1,6 +1,8 @@
# Rojo Changelog
## Unreleased
* Add auto-reconnect and improve UX for sync reminders ([#1096])
* Add support for syncing `yml` and `yaml` files (behaves similar to JSON and TOML) ([#1093])
* Fixed colors of Table diff ([#1084])
* Fixed `sourcemap` command outputting paths with OS-specific path separators ([#1085])
@@ -11,12 +13,13 @@
* Added `--absolute` flag to the sourcemap subcommand, which will emit absolute paths instead of relative paths. ([#1092])
* Fixed applying `gameId` and `placeId` before initial sync was accepted ([#1104])
[#1096]: https://github.com/rojo-rbx/rojo/pull/1096
[#1093]: https://github.com/rojo-rbx/rojo/pull/1093
[#1084]: https://github.com/rojo-rbx/rojo/pull/1084
[#1085]: https://github.com/rojo-rbx/rojo/pull/1085
[#1081]: https://github.com/rojo-rbx/rojo/pull/1081
[#1080]: https://github.com/rojo-rbx/rojo/pull/1080
[#1049]: https://github.com/rojo-rbx/rojo/pull/1066
[#1066]: https://github.com/rojo-rbx/rojo/pull/1066
[#1069]: https://github.com/rojo-rbx/rojo/pull/1069
[#1092]: https://github.com/rojo-rbx/rojo/pull/1092
[#1104]: https://github.com/rojo-rbx/rojo/pull/1104

View File

@@ -0,0 +1,151 @@
local StudioService = game:GetService("StudioService")
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local Log = require(Packages.Log)
local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets)
local TextButton = require(Plugin.App.Components.TextButton)
local e = Roact.createElement
local FullscreenNotification = Roact.Component:extend("FullscreeFullscreenNotificationnNotification")
function FullscreenNotification:init()
self.transparency, self.setTransparency = Roact.createBinding(0)
self.lifetime = self.props.timeout
end
function FullscreenNotification:dismiss()
if self.props.onClose then
self.props.onClose()
end
end
function FullscreenNotification:didMount()
self.props.soundPlayer:play(Assets.Sounds.Notification)
self.timeout = task.spawn(function()
local clock = os.clock()
local seen = false
while task.wait(1 / 10) do
local now = os.clock()
local dt = now - clock
clock = now
if not seen then
seen = StudioService.ActiveScript == nil
end
if not seen then
-- Don't run down timer before being viewed
continue
end
self.lifetime -= dt
if self.lifetime <= 0 then
self:dismiss()
break
end
end
self.timeout = nil
end)
end
function FullscreenNotification:willUnmount()
if self.timeout and coroutine.status(self.timeout) ~= "dead" then
task.cancel(self.timeout)
end
end
function FullscreenNotification:render()
return Theme.with(function(theme)
local actionButtons = {}
if self.props.actions then
for key, action in self.props.actions do
actionButtons[key] = e(TextButton, {
text = action.text,
style = action.style,
onClick = function()
self:dismiss()
if action.onClick then
local success, err = pcall(action.onClick, self)
if not success then
Log.warn("Error in notification action: " .. tostring(err))
end
end
end,
layoutOrder = -action.layoutOrder,
transparency = self.transparency,
})
end
end
return e("Frame", {
BackgroundColor3 = theme.BackgroundColor,
Size = UDim2.fromScale(1, 1),
ZIndex = self.props.layoutOrder,
}, {
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 17),
PaddingRight = UDim.new(0, 15),
PaddingTop = UDim.new(0, 10),
PaddingBottom = UDim.new(0, 10),
}),
Layout = e("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
Padding = UDim.new(0, 10),
}),
Logo = e("ImageLabel", {
ImageTransparency = self.transparency,
Image = Assets.Images.Logo,
ImageColor3 = theme.Header.LogoColor,
BackgroundTransparency = 1,
Size = UDim2.fromOffset(60, 27),
LayoutOrder = 1,
}),
Info = e("TextLabel", {
Text = self.props.text,
FontFace = theme.Font.Main,
TextSize = theme.TextSize.Body,
TextColor3 = theme.Notification.InfoColor,
TextTransparency = self.transparency,
TextXAlignment = Enum.TextXAlignment.Center,
TextYAlignment = Enum.TextYAlignment.Center,
TextWrapped = true,
BackgroundTransparency = 1,
AutomaticSize = Enum.AutomaticSize.Y,
Size = UDim2.fromScale(0.4, 0),
LayoutOrder = 2,
}),
Actions = if self.props.actions
then e("Frame", {
Size = UDim2.new(1, -40, 0, 37),
BackgroundTransparency = 1,
LayoutOrder = 3,
}, {
Layout = e("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 5),
}),
Buttons = Roact.createFragment(actionButtons),
})
else nil,
})
end)
end
return FullscreenNotification

View File

@@ -16,8 +16,6 @@ local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local TextButton = require(Plugin.App.Components.TextButton)
local baseClock = DateTime.now().UnixTimestampMillis
local e = Roact.createElement
local Notification = Roact.Component:extend("Notification")
@@ -77,7 +75,9 @@ function Notification:didMount()
end
function Notification:willUnmount()
if self.timeout and coroutine.status(self.timeout) ~= "dead" then
task.cancel(self.timeout)
end
end
function Notification:render()
@@ -95,10 +95,13 @@ function Notification:render()
text = action.text,
style = action.style,
onClick = function()
self:dismiss()
if action.onClick then
local success, err = pcall(action.onClick, self)
if not success then
Log.warn("Error in notification action: " .. tostring(err))
end
end
end,
layoutOrder = -action.layoutOrder,
transparency = transparency,
@@ -138,17 +141,17 @@ function Notification:render()
}, {
e(BorderedContainer, {
transparency = transparency,
size = UDim2.new(1, 0, 1, 0),
size = UDim2.fromScale(1, 1),
}, {
Contents = e("Frame", {
Size = UDim2.new(1, 0, 1, 0),
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
}, {
Logo = e("ImageLabel", {
ImageTransparency = transparency,
Image = Assets.Images.PluginButton,
BackgroundTransparency = 1,
Size = UDim2.new(0, logoSize, 0, logoSize),
Size = UDim2.fromOffset(logoSize, logoSize),
Position = UDim2.new(0, 0, 0, 0),
AnchorPoint = Vector2.new(0, 0),
}),
@@ -171,7 +174,7 @@ function Notification:render()
Actions = if self.props.actions
then e("Frame", {
Size = UDim2.new(1, -40, 0, actionsY),
Position = UDim2.new(1, 0, 1, 0),
Position = UDim2.fromScale(1, 1),
AnchorPoint = Vector2.new(1, 1),
BackgroundTransparency = 1,
}, {
@@ -198,26 +201,4 @@ function Notification:render()
end)
end
local Notifications = Roact.Component:extend("Notifications")
function Notifications:render()
local notifs = {}
for id, notif in self.props.notifications do
notifs["NotifID_" .. id] = e(Notification, {
soundPlayer = self.props.soundPlayer,
text = notif.text,
timestamp = notif.timestamp,
timeout = notif.timeout,
actions = notif.actions,
layoutOrder = (notif.timestamp - baseClock),
onClose = function()
self.props.onClose(id)
end,
})
end
return Roact.createFragment(notifs)
end
return Notifications
return Notification

View File

@@ -0,0 +1,66 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local e = Roact.createElement
local Notification = require(script.Notification)
local FullscreenNotification = require(script.FullscreenNotification)
local Notifications = Roact.Component:extend("Notifications")
function Notifications:render()
local popupNotifs = {}
local fullscreenNotifs = {}
for id, notif in self.props.notifications do
local targetTable = if notif.isFullscreen then fullscreenNotifs else popupNotifs
local targetComponent = if notif.isFullscreen then FullscreenNotification else Notification
targetTable["NotifID_" .. id] = e(targetComponent, {
soundPlayer = self.props.soundPlayer,
text = notif.text,
timeout = notif.timeout,
actions = notif.actions,
layoutOrder = id,
onClose = function()
if notif.onClose then
notif.onClose()
end
self.props.onClose(id)
end,
})
end
return e("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
}, {
Fullscreen = e("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
}, {
notifs = Roact.createFragment(fullscreenNotifs),
}),
Popups = e("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
}, {
Layout = e("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
VerticalAlignment = Enum.VerticalAlignment.Bottom,
Padding = UDim.new(0, 5),
}),
Padding = e("UIPadding", {
PaddingTop = UDim.new(0, 5),
PaddingBottom = UDim.new(0, 5),
PaddingLeft = UDim.new(0, 5),
PaddingRight = UDim.new(0, 5),
}),
notifs = Roact.createFragment(popupNotifs),
}),
})
end
return Notifications

View File

@@ -27,6 +27,7 @@ end
local invertedLevels = invertTbl(Log.Level)
local confirmationBehaviors = { "Initial", "Always", "Large Changes", "Unlisted PlaceId", "Never" }
local syncReminderModes = { "None", "Notify", "Fullscreen" }
local function Navbar(props)
return Theme.with(function(theme)
@@ -93,6 +94,14 @@ function SettingsPage:render()
contentSize = self.contentSize,
transparency = self.props.transparency,
}, {
AutoReconnect = e(Setting, {
id = "autoReconnect",
name = "Auto Reconnect",
description = "Reconnect to server on place open if the served project matches the last sync to the place",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
}),
ShowNotifications = e(Setting, {
id = "showNotifications",
name = "Show Notifications",
@@ -101,13 +110,26 @@ function SettingsPage:render()
layoutOrder = layoutIncrement(),
}),
SyncReminder = e(Setting, {
id = "syncReminder",
SyncReminderMode = e(Setting, {
id = "syncReminderMode",
name = "Sync Reminder",
description = "Notify to sync when opening a place that has previously been synced",
description = "What type of reminders you receive for syncing your project",
transparency = self.props.transparency,
visible = Settings:getBinding("showNotifications"),
layoutOrder = layoutIncrement(),
visible = Settings:getBinding("showNotifications"),
options = syncReminderModes,
}),
SyncReminderPolling = e(Setting, {
id = "syncReminderPolling",
name = "Sync Reminder Polling",
description = "Look for available sync servers periodically",
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
visible = Settings:getBindings("syncReminderMode", "showNotifications"):map(function(values)
return values.syncReminderMode ~= "None" and values.showNotifications
end),
}),
ConfirmationBehavior = e(Setting, {

View File

@@ -9,6 +9,7 @@ local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local Log = require(Packages.Log)
local Promise = require(Packages.Promise)
local Assets = require(Plugin.Assets)
local Version = require(Plugin.Version)
@@ -27,7 +28,7 @@ local timeUtil = require(Plugin.timeUtil)
local Theme = require(script.Theme)
local Page = require(script.Page)
local Notifications = require(script.Notifications)
local Notifications = require(script.Components.Notifications)
local Tooltip = require(script.Components.Tooltip)
local StudioPluginAction = require(script.Components.Studio.StudioPluginAction)
local StudioToolbar = require(script.Components.Studio.StudioToolbar)
@@ -78,17 +79,18 @@ function App:init()
action
)
)
local dismissNotif = self:addNotification(
string.format("You've undone '%s'.\nIf this was not intended, please restore.", action),
10,
{
local dismissNotif = self:addNotification({
text = string.format("You've undone '%s'.\nIf this was not intended, please restore.", action),
timeout = 10,
onClose = function()
cleanup()
end,
actions = {
Restore = {
text = "Restore",
style = "Solid",
layoutOrder = 1,
onClick = function(notification)
cleanup()
notification:dismiss()
onClick = function()
ChangeHistoryService:Redo()
end,
},
@@ -96,13 +98,9 @@ function App:init()
text = "Dismiss",
style = "Bordered",
layoutOrder = 2,
onClick = function(notification)
cleanup()
notification:dismiss()
end,
},
}
)
},
})
undoConnection = ChangeHistoryService.OnUndo:Once(function()
-- Our notif is now out of date- redoing will not restore the patch
@@ -142,42 +140,20 @@ function App:init()
if RunService:IsEdit() then
self:checkForUpdates()
if
Settings:get("syncReminder")
and self.serveSession == nil
and self:getPriorSyncInfo().timestamp ~= nil
and (self:isSyncLockAvailable())
then
local syncInfo = self:getPriorSyncInfo()
local timeSinceSync = timeUtil.elapsedToText(os.time() - syncInfo.timestamp)
local syncDetail = if syncInfo.projectName
then `project '{syncInfo.projectName}'`
else `{syncInfo.host or Config.defaultHost}:{syncInfo.port or Config.defaultPort}`
self:addNotification(
`You synced {syncDetail} to this place {timeSinceSync}. Would you like to reconnect?`,
300,
{
Connect = {
text = "Connect",
style = "Solid",
layoutOrder = 1,
onClick = function(notification)
notification:dismiss()
self:startSession()
end,
},
Dismiss = {
text = "Dismiss",
style = "Bordered",
layoutOrder = 2,
onClick = function(notification)
notification:dismiss()
end,
},
}
)
self:startSyncReminderPolling()
self.disconnectSyncReminderPollingChanged = Settings:onChanged("syncReminderPolling", function(enabled)
if enabled then
self:startSyncReminderPolling()
else
self:stopSyncReminderPolling()
end
end)
self:tryAutoReconnect():andThen(function(didReconnect)
if not didReconnect then
self:checkSyncReminder()
end
end)
end
if self:isAutoConnectPlaytestServerAvailable() then
@@ -203,16 +179,23 @@ function App:willUnmount()
self.disconnectUpdatesCheckChanged()
self.disconnectPrereleasesCheckChanged()
if self.disconnectSyncReminderPollingChanged then
self.disconnectSyncReminderPollingChanged()
end
self:stopSyncReminderPolling()
self.autoConnectPlaytestServerListener()
self:clearRunningConnectionInfo()
end
function App:addNotification(
function App:addNotification(notif: {
text: string,
isFullscreen: boolean?,
timeout: number?,
actions: { [string]: { text: string, style: string, layoutOrder: number, onClick: (any) -> () } }?
)
actions: { [string]: { text: string, style: string, layoutOrder: number, onClick: (any) -> ()? } }?,
onClose: (any) -> ()?,
})
if not Settings:get("showNotifications") then
return
end
@@ -220,17 +203,17 @@ function App:addNotification(
self.notifId += 1
local id = self.notifId
local notifications = table.clone(self.state.notifications)
notifications[id] = {
text = text,
timestamp = DateTime.now().UnixTimestampMillis,
timeout = timeout or 3,
actions = actions,
}
self:setState(function(prevState)
local notifications = table.clone(prevState.notifications)
notifications[id] = Dictionary.merge({
timeout = notif.timeout or 5,
isFullscreen = notif.isFullscreen or false,
}, notif)
self:setState({
return {
notifications = notifications,
})
}
end)
return function()
self:closeNotification(id)
@@ -242,26 +225,29 @@ function App:closeNotification(id: number)
return
end
local notifications = table.clone(self.state.notifications)
self:setState(function(prevState)
local notifications = table.clone(prevState.notifications)
notifications[id] = nil
self:setState({
return {
notifications = notifications,
})
}
end)
end
function App:checkForUpdates()
local updateMessage = Version.getUpdateMessage()
if updateMessage then
self:addNotification(updateMessage, 500, {
self:addNotification({
text = updateMessage,
timeout = 500,
actions = {
Dismiss = {
text = "Dismiss",
style = "Bordered",
layoutOrder = 2,
onClick = function(notification)
notification:dismiss()
end,
},
},
})
end
@@ -385,6 +371,155 @@ function App:releaseSyncLock()
Log.trace("Could not relase sync lock because it is owned by {}", lock.Value)
end
function App:findActiveServer()
local host, port = self:getHostAndPort()
local baseUrl = if string.find(host, "^https?://")
then string.format("%s:%s", host, port)
else string.format("http://%s:%s", host, port)
Log.trace("Checking for active sync server at {}", baseUrl)
local apiContext = ApiContext.new(baseUrl)
return apiContext:connect():andThen(function(serverInfo)
apiContext:disconnect()
return serverInfo, host, port
end)
end
function App:tryAutoReconnect()
if not Settings:get("autoReconnect") then
return Promise.resolve(false)
end
local priorSyncInfo = self:getPriorSyncInfo()
if not priorSyncInfo.projectName then
Log.trace("No prior sync info found, skipping auto-reconnect")
return Promise.resolve(false)
end
return self:findActiveServer()
:andThen(function(serverInfo)
-- change
if serverInfo.projectName == priorSyncInfo.projectName then
Log.trace("Auto-reconnect found matching server, reconnecting...")
self:addNotification({
text = `Auto-reconnect discovered project '{serverInfo.projectName}'...`,
})
self:startSession()
return true
end
Log.trace("Auto-reconnect found different server, not reconnecting")
return false
end)
:catch(function()
Log.trace("Auto-reconnect did not find a server, not reconnecting")
return false
end)
end
function App:checkSyncReminder()
local syncReminderMode = Settings:get("syncReminderMode")
if syncReminderMode == "None" then
return
end
if self.serveSession ~= nil or not self:isSyncLockAvailable() then
-- Already syncing or cannot sync, no reason to remind
return
end
local priorSyncInfo = self:getPriorSyncInfo()
self:findActiveServer()
:andThen(function(serverInfo, host, port)
self:sendSyncReminder(
`Project '{serverInfo.projectName}' is serving at {host}:{port}.\nWould you like to connect?`
)
end)
:catch(function()
if priorSyncInfo.timestamp and priorSyncInfo.projectName then
-- We didn't find an active server,
-- but this place has a prior sync
-- so we should remind the user to serve
local timeSinceSync = timeUtil.elapsedToText(os.time() - priorSyncInfo.timestamp)
self:sendSyncReminder(
`You synced project '{priorSyncInfo.projectName}' to this place {timeSinceSync}.\nDid you mean to run 'rojo serve' and then connect?`
)
end
end)
end
function App:startSyncReminderPolling()
if
self.syncReminderPollingThread ~= nil
or Settings:get("syncReminderMode") == "None"
or not Settings:get("syncReminderPolling")
then
return
end
Log.trace("Starting sync reminder polling thread")
self.syncReminderPollingThread = task.spawn(function()
while task.wait(30) do
if self.syncReminderPollingThread == nil then
-- The polling thread was stopped, so exit
return
end
if self.dismissSyncReminder then
-- There is already a sync reminder being shown
task.wait(5)
continue
end
self:checkSyncReminder()
end
end)
end
function App:stopSyncReminderPolling()
if self.syncReminderPollingThread then
Log.trace("Stopping sync reminder polling thread")
task.cancel(self.syncReminderPollingThread)
self.syncReminderPollingThread = nil
end
end
function App:sendSyncReminder(message: string)
local syncReminderMode = Settings:get("syncReminderMode")
if syncReminderMode == "None" then
return
end
self.dismissSyncReminder = self:addNotification({
text = message,
timeout = 120,
isFullscreen = Settings:get("syncReminderMode") == "Fullscreen",
onClose = function()
self.dismissSyncReminder = nil
end,
actions = {
Connect = {
text = "Connect",
style = "Solid",
layoutOrder = 1,
onClick = function()
self:startSession()
end,
},
Dismiss = {
text = "Dismiss",
style = "Bordered",
layoutOrder = 2,
onClick = function()
-- If the user dismisses the reminder,
-- then we don't need to remind them again
self:stopSyncReminderPolling()
end,
},
},
})
end
function App:isAutoConnectPlaytestServerAvailable()
return RunService:IsRunning()
and RunService:IsStudio()
@@ -435,7 +570,10 @@ function App:startSession()
local msg = string.format("Could not sync because user '%s' is already syncing", tostring(priorOwner))
Log.warn(msg)
self:addNotification(msg, 10)
self:addNotification({
text = msg,
timeout = 10,
})
self:setState({
appStatus = AppStatus.Error,
errorMessage = msg,
@@ -506,11 +644,18 @@ function App:startSession()
serveSession:onStatusChanged(function(status, details)
if status == ServeSession.Status.Connecting then
if self.dismissSyncReminder then
self.dismissSyncReminder()
self.dismissSyncReminder = nil
end
self:setState({
appStatus = AppStatus.Connecting,
toolbarIcon = Assets.Images.PluginButton,
})
self:addNotification("Connecting to session...")
self:addNotification({
text = "Connecting to session...",
})
elseif status == ServeSession.Status.Connected then
self.knownProjects[details] = true
self:setPriorSyncInfo(host, port, details)
@@ -523,7 +668,9 @@ function App:startSession()
address = address,
toolbarIcon = Assets.Images.PluginButtonConnected,
})
self:addNotification(string.format("Connected to session '%s' at %s.", details, address), 5)
self:addNotification({
text = string.format("Connected to session '%s' at %s.", details, address),
})
elseif status == ServeSession.Status.Disconnected then
self.serveSession = nil
self:releaseSyncLock()
@@ -546,13 +693,19 @@ function App:startSession()
errorMessage = tostring(details),
toolbarIcon = Assets.Images.PluginButtonWarning,
})
self:addNotification(tostring(details), 10)
self:addNotification({
text = tostring(details),
timeout = 10,
})
else
self:setState({
appStatus = AppStatus.NotConnected,
toolbarIcon = Assets.Images.PluginButton,
})
self:addNotification("Disconnected from session.")
self:addNotification({
text = "Disconnected from session.",
timeout = 10,
})
end
end
end)
@@ -630,13 +783,13 @@ function App:startSession()
toolbarIcon = Assets.Images.PluginButton,
})
self:addNotification(
string.format(
self:addNotification({
text = string.format(
"Please accept%sor abort the initializing sync session.",
Settings:get("twoWaySync") and ", reject, " or " "
),
7
)
timeout = 7,
})
return self.confirmationEvent:Wait()
end)
@@ -797,19 +950,7 @@ function App:render()
ResetOnSpawn = false,
DisplayOrder = 100,
}, {
layout = e("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
VerticalAlignment = Enum.VerticalAlignment.Bottom,
Padding = UDim.new(0, 5),
}),
padding = e("UIPadding", {
PaddingTop = UDim.new(0, 5),
PaddingBottom = UDim.new(0, 5),
PaddingLeft = UDim.new(0, 5),
PaddingRight = UDim.new(0, 5),
}),
notifs = e(Notifications, {
Notifications = e(Notifications, {
soundPlayer = self.props.soundPlayer,
notifications = self.state.notifications,
onClose = function(id)

View File

@@ -12,12 +12,14 @@ local Roact = require(Packages.Roact)
local defaultSettings = {
openScriptsExternally = false,
twoWaySync = false,
autoReconnect = false,
showNotifications = true,
syncReminder = true,
syncReminderMode = "Notify" :: "None" | "Notify" | "Fullscreen",
syncReminderPolling = true,
checkForUpdates = true,
checkForPrereleases = false,
autoConnectPlaytestServer = false,
confirmationBehavior = "Initial",
confirmationBehavior = "Initial" :: "Never" | "Initial" | "Large Changes" | "Unlisted PlaceId",
largeChangesConfirmationThreshold = 5,
playSounds = true,
typecheckingEnabled = false,
@@ -108,4 +110,14 @@ function Settings:getBinding(name)
return bind
end
function Settings:getBindings(...: string)
local bindings = {}
for i = 1, select("#", ...) do
local source = select(i, ...)
bindings[source] = self:getBinding(source)
end
return Roact.joinBindings(bindings)
end
return Settings