diff --git a/CHANGELOG.md b/CHANGELOG.md index eb44c444..a71d618e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/plugin/src/App/Components/Notifications/FullscreenNotification.lua b/plugin/src/App/Components/Notifications/FullscreenNotification.lua new file mode 100644 index 00000000..215398b2 --- /dev/null +++ b/plugin/src/App/Components/Notifications/FullscreenNotification.lua @@ -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 diff --git a/plugin/src/App/Notifications.lua b/plugin/src/App/Components/Notifications/Notification.lua similarity index 84% rename from plugin/src/App/Notifications.lua rename to plugin/src/App/Components/Notifications/Notification.lua index 0d2129a4..47fb0d08 100644 --- a/plugin/src/App/Notifications.lua +++ b/plugin/src/App/Components/Notifications/Notification.lua @@ -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() - task.cancel(self.timeout) + if self.timeout and coroutine.status(self.timeout) ~= "dead" then + task.cancel(self.timeout) + end end function Notification:render() @@ -95,9 +95,12 @@ function Notification:render() text = action.text, style = action.style, onClick = function() - local success, err = pcall(action.onClick, self) - if not success then - Log.warn("Error in notification action: " .. tostring(err)) + 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, @@ -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 diff --git a/plugin/src/App/Components/Notifications/init.lua b/plugin/src/App/Components/Notifications/init.lua new file mode 100644 index 00000000..1cc544d5 --- /dev/null +++ b/plugin/src/App/Components/Notifications/init.lua @@ -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 diff --git a/plugin/src/App/StatusPages/Settings/init.lua b/plugin/src/App/StatusPages/Settings/init.lua index 1f9af0c0..7d05fd63 100644 --- a/plugin/src/App/StatusPages/Settings/init.lua +++ b/plugin/src/App/StatusPages/Settings/init.lua @@ -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, { diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index 74235656..7ac84f60 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -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:startSyncReminderPolling() + self.disconnectSyncReminderPollingChanged = Settings:onChanged("syncReminderPolling", function(enabled) + if enabled then + self:startSyncReminderPolling() + else + self:stopSyncReminderPolling() + end + end) - 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, - }, - } - ) - 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({ - notifications = notifications, - }) + 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) - notifications[id] = nil + self:setState(function(prevState) + local notifications = table.clone(prevState.notifications) + notifications[id] = nil - self:setState({ - notifications = notifications, - }) + return { + notifications = notifications, + } + end) end function App:checkForUpdates() local updateMessage = Version.getUpdateMessage() if updateMessage then - self:addNotification(updateMessage, 500, { - Dismiss = { - text = "Dismiss", - style = "Bordered", - layoutOrder = 2, - onClick = function(notification) - notification:dismiss() - end, + self:addNotification({ + text = updateMessage, + timeout = 500, + actions = { + Dismiss = { + text = "Dismiss", + style = "Bordered", + layoutOrder = 2, + }, }, }) 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) diff --git a/plugin/src/Settings.lua b/plugin/src/Settings.lua index 7810635b..08f7e081 100644 --- a/plugin/src/Settings.lua +++ b/plugin/src/Settings.lua @@ -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