diff --git a/CHANGELOG.md b/CHANGELOG.md index b73a903c..d426619d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Rojo Changelog +## Unreleased + +* Added an update indicator to the version header when a new version of the plugin is available. ([#1069]) + +[#1069]: https://github.com/rojo-rbx/rojo/pull/1069 + ## 7.5.1 - April 25th, 2025 * Fixed output spam related to `Instance.Capabilities` in the plugin diff --git a/plugin/src/App/Components/Header.lua b/plugin/src/App/Components/Header.lua index 81e2f46a..fa8c9173 100644 --- a/plugin/src/App/Components/Header.lua +++ b/plugin/src/App/Components/Header.lua @@ -9,8 +9,70 @@ local Assets = require(Plugin.Assets) local Config = require(Plugin.Config) local Version = require(Plugin.Version) +local Tooltip = require(Plugin.App.Components.Tooltip) +local SlicedImage = require(script.Parent.SlicedImage) + local e = Roact.createElement +local function VersionIndicator(props) + local updateMessage = Version.getUpdateMessage() + + return Theme.with(function(theme) + return e("Frame", { + LayoutOrder = props.layoutOrder, + Size = UDim2.new(0, 0, 0, 25), + BackgroundTransparency = 1, + AutomaticSize = Enum.AutomaticSize.X, + }, { + Border = if updateMessage + then e(SlicedImage, { + slice = Assets.Slices.RoundedBorder, + color = theme.Button.Bordered.Enabled.BorderColor, + transparency = props.transparency, + size = UDim2.fromScale(1, 1), + zIndex = 0, + }, { + Indicator = e("ImageLabel", { + Size = UDim2.new(0, 10, 0, 10), + ScaleType = Enum.ScaleType.Fit, + Image = Assets.Images.Circles[16], + ImageColor3 = theme.Header.LogoColor, + ImageTransparency = props.transparency, + BackgroundTransparency = 1, + Position = UDim2.new(1, 0, 0, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + }), + }) + else nil, + + Tip = if updateMessage + then e(Tooltip.Trigger, { + text = updateMessage, + delay = 0.1, + }) + else nil, + + VersionText = e("TextLabel", { + Text = Version.display(Config.version), + FontFace = theme.Font.Thin, + TextSize = theme.TextSize.Body, + TextColor3 = theme.Header.VersionColor, + TextXAlignment = Enum.TextXAlignment.Left, + TextTransparency = props.transparency, + BackgroundTransparency = 1, + + Size = UDim2.new(0, 0, 1, 0), + AutomaticSize = Enum.AutomaticSize.X, + }, { + Padding = e("UIPadding", { + PaddingLeft = UDim.new(0, 6), + PaddingRight = UDim.new(0, 6), + }), + }), + }) + end) +end + local function Header(props) return Theme.with(function(theme) return e("Frame", { @@ -29,18 +91,9 @@ local function Header(props) BackgroundTransparency = 1, }), - Version = e("TextLabel", { - Text = Version.display(Config.version), - FontFace = theme.Font.Thin, - TextSize = theme.TextSize.Body, - TextColor3 = theme.Header.VersionColor, - TextXAlignment = Enum.TextXAlignment.Left, - TextTransparency = props.transparency, - - Size = UDim2.new(1, 0, 0, theme.TextSize.Body), - - LayoutOrder = 2, - BackgroundTransparency = 1, + VersionIndicator = e(VersionIndicator, { + transparency = props.transparency, + layoutOrder = 2, }), Layout = e("UIListLayout", { diff --git a/plugin/src/App/Components/Tooltip.lua b/plugin/src/App/Components/Tooltip.lua index 25423e8c..49d37af7 100644 --- a/plugin/src/App/Components/Tooltip.lua +++ b/plugin/src/App/Components/Tooltip.lua @@ -216,7 +216,7 @@ function Trigger:managePopup() return end - self.showDelayThread = task.delay(DELAY, function() + self.showDelayThread = task.delay(self.props.delay or DELAY, function() self.props.context.addTip(self.id, { Text = self.props.text, Position = self:getMousePos(), diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index df1bccac..828f7299 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -251,27 +251,10 @@ function App:closeNotification(id: number) end function App:checkForUpdates() - if not Settings:get("checkForUpdates") then - return - end + local updateMessage = Version.getUpdateMessage() - local isLocalInstall = string.find(debug.traceback(), "\n[^\n]-user_.-$") ~= nil - local latestCompatibleVersion = Version.retrieveLatestCompatible({ - version = Config.version, - includePrereleases = isLocalInstall and Settings:get("checkForPrereleases"), - }) - if not latestCompatibleVersion then - return - end - - self:addNotification( - string.format( - "A newer compatible version of Rojo, %s, was published %s! Go to the Rojo releases page to learn more.", - Version.display(latestCompatibleVersion.version), - timeUtil.elapsedToText(DateTime.now().UnixTimestamp - latestCompatibleVersion.publishedUnixTimestamp) - ), - 500, - { + if updateMessage then + self:addNotification(updateMessage, 500, { Dismiss = { text = "Dismiss", style = "Bordered", @@ -280,8 +263,8 @@ function App:checkForUpdates() notification:dismiss() end, }, - } - ) + }) + end end function App:getPriorSyncInfo(): { host: string?, port: string?, projectName: string?, timestamp: number? } diff --git a/plugin/src/Version.lua b/plugin/src/Version.lua index d95702e8..197de133 100644 --- a/plugin/src/Version.lua +++ b/plugin/src/Version.lua @@ -1,6 +1,20 @@ -local Packages = script.Parent.Parent.Packages +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin +local Packages = Rojo.Packages + local Http = require(Packages.Http) local Promise = require(Packages.Promise) +local Log = require(Packages.Log) + +local Config = require(Plugin.Config) +local Settings = require(Plugin.Settings) +local timeUtil = require(Plugin.timeUtil) + +type LatestReleaseInfo = { + version: { number }, + prerelease: boolean, + publishedUnixTimestamp: number, +} local function compare(a, b) if a > b then @@ -88,14 +102,26 @@ function Version.display(version) return output end +--[[ + The GitHub API rate limit for unauthenticated requests is rather low, + and we don't release often enough to warrant checking it more than once a day. +--]] +Version._cachedLatestCompatible = nil :: { + value: LatestReleaseInfo?, + timestamp: number, +}? + function Version.retrieveLatestCompatible(options: { version: { number }, includePrereleases: boolean?, -}): { - version: { number }, - prerelease: boolean, - publishedUnixTimestamp: number, -}? +}): LatestReleaseInfo? + if Version._cachedLatestCompatible and os.clock() - Version._cachedLatestCompatible.timestamp < 60 * 60 * 24 then + Log.debug("Using cached latest compatible version") + return Version._cachedLatestCompatible.value + end + + Log.debug("Retrieving latest compatible version from GitHub") + local success, releases = Http.get("https://api.github.com/repos/rojo-rbx/rojo/releases?per_page=10") :andThen(function(response) if response.code >= 400 then @@ -114,7 +140,7 @@ function Version.retrieveLatestCompatible(options: { end -- Iterate through releases, looking for the latest compatible version - local latestCompatible = nil + local latestCompatible: LatestReleaseInfo? = nil for _, release in releases do -- Skip prereleases if they are not requested if (not options.includePrereleases) and release.prerelease then @@ -142,10 +168,43 @@ function Version.retrieveLatestCompatible(options: { -- Don't return anything if the latest found is not newer than the current version if latestCompatible == nil or Version.compare(latestCompatible.version, options.version) <= 0 then + -- Cache as nil so we don't try again for a day + Version._cachedLatestCompatible = { + value = nil, + timestamp = os.clock(), + } + return nil end + -- Cache the latest compatible version + Version._cachedLatestCompatible = { + value = latestCompatible, + timestamp = os.clock(), + } + return latestCompatible end +function Version.getUpdateMessage(): string? + if not Settings:get("checkForUpdates") then + return + end + + local isLocalInstall = string.find(debug.traceback(), "\n[^\n]-user_.-$") ~= nil + local latestCompatibleVersion = Version.retrieveLatestCompatible({ + version = Config.version, + includePrereleases = isLocalInstall and Settings:get("checkForPrereleases"), + }) + if not latestCompatibleVersion then + return + end + + return string.format( + "A newer compatible version of Rojo, %s, was published %s! Go to the Rojo releases page to learn more.", + Version.display(latestCompatibleVersion.version), + timeUtil.elapsedToText(DateTime.now().UnixTimestamp - latestCompatibleVersion.publishedUnixTimestamp) + ) +end + return Version