From 4b5db4e5a93f9f57f36f59947b13e5b7b76a01e8 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Mon, 5 Aug 2024 11:34:29 -0700 Subject: [PATCH] Check for compatible updates in plugin (#832) --- CHANGELOG.md | 2 + plugin/src/App/StatusPages/Connected.lua | 27 +---- plugin/src/App/StatusPages/Settings/init.lua | 19 ++++ plugin/src/App/init.lua | 101 ++++++++++++----- plugin/src/Settings.lua | 2 + plugin/src/Version.lua | 107 ++++++++++++++++++- plugin/src/Version.spec.lua | 35 ++++++ plugin/src/timeUtil.lua | 31 ++++++ 8 files changed, 273 insertions(+), 51 deletions(-) create mode 100644 plugin/src/timeUtil.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index 13fed6f2..dec82d6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ * Added popout diff visualizer for table properties like Attributes and Tags ([#834]) * Updated Theme to use Studio colors ([#838]) * Improved patch visualizer UX ([#883]) +* Added update notifications for newer compatible versions in the Studio plugin. ([#832]) * Added experimental setting for Auto Connect in playtests ([#840]) * Improved settings UI ([#886]) * `Open Scripts Externally` option can now be changed while syncing ([#911]) @@ -75,6 +76,7 @@ **All** sync rules are reset between project files, so they must be specified in each one when nesting them. This is to ensure that nothing can break other projects by changing how files are synced! [#813]: https://github.com/rojo-rbx/rojo/pull/813 +[#832]: https://github.com/rojo-rbx/rojo/pull/832 [#834]: https://github.com/rojo-rbx/rojo/pull/834 [#838]: https://github.com/rojo-rbx/rojo/pull/838 [#840]: https://github.com/rojo-rbx/rojo/pull/840 diff --git a/plugin/src/App/StatusPages/Connected.lua b/plugin/src/App/StatusPages/Connected.lua index 3f84d15f..b0bf33d9 100644 --- a/plugin/src/App/StatusPages/Connected.lua +++ b/plugin/src/App/StatusPages/Connected.lua @@ -4,6 +4,7 @@ local Packages = Rojo.Packages local Roact = require(Packages.Roact) +local timeUtil = require(Plugin.timeUtil) local Theme = require(Plugin.App.Theme) local Assets = require(Plugin.Assets) local PatchSet = require(Plugin.PatchSet) @@ -20,28 +21,6 @@ local TableDiffVisualizer = require(Plugin.App.Components.TableDiffVisualizer) local e = Roact.createElement -local AGE_UNITS = { - { 31556909, "y" }, - { 2629743, "mon" }, - { 604800, "w" }, - { 86400, "d" }, - { 3600, "h" }, - { 60, "m" }, -} -function timeSinceText(elapsed: number): string - local ageText = string.format("%ds", elapsed) - - for _, UnitData in ipairs(AGE_UNITS) do - local UnitSeconds, UnitName = UnitData[1], UnitData[2] - if elapsed > UnitSeconds then - ageText = elapsed // UnitSeconds .. UnitName - break - end - end - - return ageText -end - local ChangesViewer = Roact.Component:extend("ChangesViewer") function ChangesViewer:init() @@ -287,7 +266,7 @@ function ConnectedPage:getChangeInfoText() if patchData == nil then return "" end - return timeSinceText(DateTime.now().UnixTimestamp - patchData.timestamp) + return timeUtil.elapsedToText(DateTime.now().UnixTimestamp - patchData.timestamp) end function ConnectedPage:startChangeInfoTextUpdater() @@ -303,7 +282,7 @@ function ConnectedPage:startChangeInfoTextUpdater() local updateInterval = 1 -- Update timestamp text as frequently as currently needed - for _, UnitData in ipairs(AGE_UNITS) do + for _, UnitData in ipairs(timeUtil.AGE_UNITS) do local UnitSeconds = UnitData[1] if elapsed > UnitSeconds then updateInterval = UnitSeconds diff --git a/plugin/src/App/StatusPages/Settings/init.lua b/plugin/src/App/StatusPages/Settings/init.lua index 70f83508..122a9f28 100644 --- a/plugin/src/App/StatusPages/Settings/init.lua +++ b/plugin/src/App/StatusPages/Settings/init.lua @@ -162,6 +162,25 @@ function SettingsPage:render() layoutOrder = layoutIncrement(), }), + CheckForUpdates = e(Setting, { + id = "checkForUpdates", + name = "Check For Updates", + description = "Notify about newer compatible Rojo releases", + transparency = self.props.transparency, + layoutOrder = layoutIncrement(), + }), + + CheckForPreleases = e(Setting, { + id = "checkForPrereleases", + name = "Include Prerelease Updates", + description = "Include prereleases when checking for updates", + transparency = self.props.transparency, + layoutOrder = layoutIncrement(), + visible = if string.find(debug.traceback(), "\n[^\n]-user_.-$") == nil + then false -- Must be a local install to allow prerelease checks + else Settings:getBinding("checkForUpdates"), + }), + AutoConnectPlaytestServer = e(Setting, { id = "autoConnectPlaytestServer", name = "Auto Connect Playtest Server", diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index bd21765d..e3f98725 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -23,6 +23,7 @@ local PatchTree = require(Plugin.PatchTree) local preloadAssets = require(Plugin.preloadAssets) local soundPlayer = require(Plugin.soundPlayer) local ignorePlaceIds = require(Plugin.ignorePlaceIds) +local timeUtil = require(Plugin.timeUtil) local Theme = require(script.Theme) local Page = require(script.Page) @@ -118,6 +119,13 @@ function App:init() end) end) + self.disconnectUpdatesCheckChanged = Settings:onChanged("checkForUpdates", function() + self:checkForUpdates() + end) + self.disconnectPrereleasesCheckChanged = Settings:onChanged("checkForPrereleases", function() + self:checkForUpdates() + end) + self:setState({ appStatus = AppStatus.NotConnected, guiEnabled = false, @@ -131,32 +139,35 @@ function App:init() toolbarIcon = Assets.Images.PluginButton, }) - if - RunService:IsEdit() - and self.serveSession == nil - and Settings:get("syncReminder") - and self:getLastSyncTimestamp() - and (self:isSyncLockAvailable()) - then - self:addNotification("You've previously synced this place. 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, - }, - }) + if RunService:IsEdit() then + self:checkForUpdates() + + if + Settings:get("syncReminder") + and self.serveSession == nil + and self:getLastSyncTimestamp() + and (self:isSyncLockAvailable()) + then + self:addNotification("You've previously synced this place. 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 end if self:isAutoConnectPlaytestServerAvailable() then @@ -179,6 +190,10 @@ end function App:willUnmount() self.waypointConnection:Disconnect() self.confirmationBindable:Destroy() + + self.disconnectUpdatesCheckChanged() + self.disconnectPrereleasesCheckChanged() + self.autoConnectPlaytestServerListener() self:clearRunningConnectionInfo() end @@ -225,6 +240,40 @@ function App:closeNotification(id: number) }) end +function App:checkForUpdates() + 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 + + 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, + { + Dismiss = { + text = "Dismiss", + style = "Bordered", + layoutOrder = 2, + onClick = function(notification) + notification:dismiss() + end, + }, + } + ) +end + function App:getPriorEndpoint() local priorEndpoints = Settings:get("priorEndpoints") if not priorEndpoints then diff --git a/plugin/src/Settings.lua b/plugin/src/Settings.lua index 67f13ba2..7810635b 100644 --- a/plugin/src/Settings.lua +++ b/plugin/src/Settings.lua @@ -14,6 +14,8 @@ local defaultSettings = { twoWaySync = false, showNotifications = true, syncReminder = true, + checkForUpdates = true, + checkForPrereleases = false, autoConnectPlaytestServer = false, confirmationBehavior = "Initial", largeChangesConfirmationThreshold = 5, diff --git a/plugin/src/Version.lua b/plugin/src/Version.lua index 6c93a14e..d95702e8 100644 --- a/plugin/src/Version.lua +++ b/plugin/src/Version.lua @@ -1,3 +1,7 @@ +local Packages = script.Parent.Parent.Packages +local Http = require(Packages.Http) +local Promise = require(Packages.Promise) + local function compare(a, b) if a > b then return 1 @@ -30,7 +34,48 @@ function Version.compare(a, b) return minor end - return revision + if revision ~= 0 then + return revision + end + + local aPrerelease = if a[4] == "" then nil else a[4] + local bPrerelease = if b[4] == "" then nil else b[4] + + -- If neither are prerelease, they are the same + if aPrerelease == nil and bPrerelease == nil then + return 0 + end + + -- If one is prerelease it is older + if aPrerelease ~= nil and bPrerelease == nil then + return -1 + end + if aPrerelease == nil and bPrerelease ~= nil then + return 1 + end + + -- If they are both prereleases, compare those based on number + local aPrereleaseNumeric = string.match(aPrerelease, "(%d+).*$") + local bPrereleaseNumeric = string.match(bPrerelease, "(%d+).*$") + + if aPrereleaseNumeric == nil or bPrereleaseNumeric == nil then + -- If one or both lack a number, comparing isn't meaningful + return 0 + end + return compare(tonumber(aPrereleaseNumeric) or 0, tonumber(bPrereleaseNumeric) or 0) +end + +function Version.parse(versionString: string) + local version = { string.match(versionString, "^v?(%d+)%.(%d+)%.(%d+)(.*)$") } + for i, v in version do + version[i] = tonumber(v) or v + end + + if version[4] == "" then + version[4] = nil + end + + return version end function Version.display(version) @@ -43,4 +88,64 @@ function Version.display(version) return output end +function Version.retrieveLatestCompatible(options: { + version: { number }, + includePrereleases: boolean?, +}): { + version: { number }, + prerelease: boolean, + publishedUnixTimestamp: number, +}? + 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 + local message = string.format("HTTP %s:\n%s", tostring(response.code), response.body) + + return Promise.reject(message) + end + + return response + end) + :andThen(Http.Response.json) + :await() + + if success == false or type(releases) ~= "table" or next(releases) ~= 1 then + return nil + end + + -- Iterate through releases, looking for the latest compatible version + local latestCompatible = nil + for _, release in releases do + -- Skip prereleases if they are not requested + if (not options.includePrereleases) and release.prerelease then + continue + end + + local releaseVersion = Version.parse(release.tag_name) + + -- Skip releases that are potentially incompatible + if releaseVersion[1] > options.version[1] then + continue + end + + -- Skip releases that are older than the latest compatible version + if latestCompatible ~= nil and Version.compare(releaseVersion, latestCompatible.version) <= 0 then + continue + end + + latestCompatible = { + version = releaseVersion, + prerelease = release.prerelease, + publishedUnixTimestamp = DateTime.fromIsoDate(release.published_at).UnixTimestamp, + } + end + + -- 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 + return nil + end + + return latestCompatible +end + return Version diff --git a/plugin/src/Version.spec.lua b/plugin/src/Version.spec.lua index cf9938d9..f1e9eba2 100644 --- a/plugin/src/Version.spec.lua +++ b/plugin/src/Version.spec.lua @@ -3,6 +3,7 @@ return function() it("should compare equal versions", function() expect(Version.compare({ 1, 2, 3 }, { 1, 2, 3 })).to.equal(0) + expect(Version.compare({ 1, 2, 3, "rc1" }, { 1, 2, 3, "rc1" })).to.equal(0) expect(Version.compare({ 0, 4, 0 }, { 0, 4 })).to.equal(0) expect(Version.compare({ 0, 0, 123 }, { 0, 0, 123 })).to.equal(0) expect(Version.compare({ 26 }, { 26 })).to.equal(0) @@ -13,6 +14,7 @@ return function() it("should compare newer, older versions", function() expect(Version.compare({ 1 }, { 0 })).to.equal(1) expect(Version.compare({ 1, 1 }, { 1, 0 })).to.equal(1) + expect(Version.compare({ 1, 2, 3 }, { 1, 2, 0 })).to.equal(1) end) it("should compare different major versions", function() @@ -25,4 +27,37 @@ return function() expect(Version.compare({ 1, 2, 3 }, { 1, 3, 2 })).to.equal(-1) expect(Version.compare({ 50, 1 }, { 50, 2 })).to.equal(-1) end) + + it("should compare different patch versions", function() + expect(Version.compare({ 1, 1, 3 }, { 1, 1, 2 })).to.equal(1) + expect(Version.compare({ 1, 1, 2 }, { 1, 1, 3 })).to.equal(-1) + expect(Version.compare({ 1, 1, 3, "-rc1" }, { 1, 1, 2, "-rc2" })).to.equal(1) + expect(Version.compare({ 1, 1, 2, "-rc5" }, { 1, 1, 3, "-alpha" })).to.equal(-1) + end) + + it("should compare prerelease tags", function() + expect(Version.compare({ 1, 0, 0, "-alpha" }, { 1, 0, 0 })).to.equal(-1) + expect(Version.compare({ 1, 0, 0 }, { 1, 0, 0, "-alpha" })).to.equal(1) + expect(Version.compare({ 1, 0, 0, "-rc1" }, { 1, 0, 0, "-rc2" })).to.equal(-1) + expect(Version.compare({ 1, 0, 0, "-rc2" }, { 1, 0, 0, "-rc1" })).to.equal(1) + + -- Non number prereleases are not compared since that isn't meaningful + expect(Version.compare({ 1, 0, 0, "-alpha" }, { 1, 0, 0, "-beta" })).to.equal(0) + end) + + it("should parse version from strings", function() + local a = Version.parse("v1.0.0") + expect(a).to.be.ok() + expect(a[1]).to.equal(1) + expect(a[2]).to.equal(0) + expect(a[3]).to.equal(0) + expect(a[4]).to.equal(nil) + + local b = Version.parse("7.3.1-rc1") + expect(b).to.be.ok() + expect(b[1]).to.equal(7) + expect(b[2]).to.equal(3) + expect(b[3]).to.equal(1) + expect(b[4]).to.equal("-rc1") + end) end diff --git a/plugin/src/timeUtil.lua b/plugin/src/timeUtil.lua new file mode 100644 index 00000000..280f72ef --- /dev/null +++ b/plugin/src/timeUtil.lua @@ -0,0 +1,31 @@ +local timeUtil = {} + +timeUtil.AGE_UNITS = table.freeze({ + { 31556909, "year" }, + { 2629743, "month" }, + { 604800, "week" }, + { 86400, "day" }, + { 3600, "hour" }, + { 60, "minute" }, +}) + +function timeUtil.elapsedToText(elapsed: number): string + if elapsed < 3 then + return "just now" + end + + local ageText = string.format("%d seconds ago", elapsed) + + for _, UnitData in timeUtil.AGE_UNITS do + local UnitSeconds, UnitName = UnitData[1], UnitData[2] + if elapsed > UnitSeconds then + local c = math.floor(elapsed / UnitSeconds) + ageText = string.format("%d %s%s ago", c, UnitName, c > 1 and "s" or "") + break + end + end + + return ageText +end + +return timeUtil