Check for compatible updates in plugin (#832)

This commit is contained in:
boatbomber
2024-08-05 11:34:29 -07:00
committed by GitHub
parent 3fa1d6b09c
commit 4b5db4e5a9
8 changed files with 273 additions and 51 deletions

View File

@@ -24,6 +24,7 @@
* Added popout diff visualizer for table properties like Attributes and Tags ([#834]) * Added popout diff visualizer for table properties like Attributes and Tags ([#834])
* Updated Theme to use Studio colors ([#838]) * Updated Theme to use Studio colors ([#838])
* Improved patch visualizer UX ([#883]) * 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]) * Added experimental setting for Auto Connect in playtests ([#840])
* Improved settings UI ([#886]) * Improved settings UI ([#886])
* `Open Scripts Externally` option can now be changed while syncing ([#911]) * `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! **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 [#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 [#834]: https://github.com/rojo-rbx/rojo/pull/834
[#838]: https://github.com/rojo-rbx/rojo/pull/838 [#838]: https://github.com/rojo-rbx/rojo/pull/838
[#840]: https://github.com/rojo-rbx/rojo/pull/840 [#840]: https://github.com/rojo-rbx/rojo/pull/840

View File

@@ -4,6 +4,7 @@ local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Packages.Roact)
local timeUtil = require(Plugin.timeUtil)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets) local Assets = require(Plugin.Assets)
local PatchSet = require(Plugin.PatchSet) local PatchSet = require(Plugin.PatchSet)
@@ -20,28 +21,6 @@ local TableDiffVisualizer = require(Plugin.App.Components.TableDiffVisualizer)
local e = Roact.createElement 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") local ChangesViewer = Roact.Component:extend("ChangesViewer")
function ChangesViewer:init() function ChangesViewer:init()
@@ -287,7 +266,7 @@ function ConnectedPage:getChangeInfoText()
if patchData == nil then if patchData == nil then
return "" return ""
end end
return timeSinceText(DateTime.now().UnixTimestamp - patchData.timestamp) return timeUtil.elapsedToText(DateTime.now().UnixTimestamp - patchData.timestamp)
end end
function ConnectedPage:startChangeInfoTextUpdater() function ConnectedPage:startChangeInfoTextUpdater()
@@ -303,7 +282,7 @@ function ConnectedPage:startChangeInfoTextUpdater()
local updateInterval = 1 local updateInterval = 1
-- Update timestamp text as frequently as currently needed -- 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] local UnitSeconds = UnitData[1]
if elapsed > UnitSeconds then if elapsed > UnitSeconds then
updateInterval = UnitSeconds updateInterval = UnitSeconds

View File

@@ -162,6 +162,25 @@ function SettingsPage:render()
layoutOrder = layoutIncrement(), 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, { AutoConnectPlaytestServer = e(Setting, {
id = "autoConnectPlaytestServer", id = "autoConnectPlaytestServer",
name = "Auto Connect Playtest Server", name = "Auto Connect Playtest Server",

View File

@@ -23,6 +23,7 @@ local PatchTree = require(Plugin.PatchTree)
local preloadAssets = require(Plugin.preloadAssets) local preloadAssets = require(Plugin.preloadAssets)
local soundPlayer = require(Plugin.soundPlayer) local soundPlayer = require(Plugin.soundPlayer)
local ignorePlaceIds = require(Plugin.ignorePlaceIds) local ignorePlaceIds = require(Plugin.ignorePlaceIds)
local timeUtil = require(Plugin.timeUtil)
local Theme = require(script.Theme) local Theme = require(script.Theme)
local Page = require(script.Page) local Page = require(script.Page)
@@ -118,6 +119,13 @@ function App:init()
end) end)
end) end)
self.disconnectUpdatesCheckChanged = Settings:onChanged("checkForUpdates", function()
self:checkForUpdates()
end)
self.disconnectPrereleasesCheckChanged = Settings:onChanged("checkForPrereleases", function()
self:checkForUpdates()
end)
self:setState({ self:setState({
appStatus = AppStatus.NotConnected, appStatus = AppStatus.NotConnected,
guiEnabled = false, guiEnabled = false,
@@ -131,32 +139,35 @@ function App:init()
toolbarIcon = Assets.Images.PluginButton, toolbarIcon = Assets.Images.PluginButton,
}) })
if if RunService:IsEdit() then
RunService:IsEdit() self:checkForUpdates()
and self.serveSession == nil
and Settings:get("syncReminder") if
and self:getLastSyncTimestamp() Settings:get("syncReminder")
and (self:isSyncLockAvailable()) and self.serveSession == nil
then and self:getLastSyncTimestamp()
self:addNotification("You've previously synced this place. Would you like to reconnect?", 300, { and (self:isSyncLockAvailable())
Connect = { then
text = "Connect", self:addNotification("You've previously synced this place. Would you like to reconnect?", 300, {
style = "Solid", Connect = {
layoutOrder = 1, text = "Connect",
onClick = function(notification) style = "Solid",
notification:dismiss() layoutOrder = 1,
self:startSession() onClick = function(notification)
end, notification:dismiss()
}, self:startSession()
Dismiss = { end,
text = "Dismiss", },
style = "Bordered", Dismiss = {
layoutOrder = 2, text = "Dismiss",
onClick = function(notification) style = "Bordered",
notification:dismiss() layoutOrder = 2,
end, onClick = function(notification)
}, notification:dismiss()
}) end,
},
})
end
end end
if self:isAutoConnectPlaytestServerAvailable() then if self:isAutoConnectPlaytestServerAvailable() then
@@ -179,6 +190,10 @@ end
function App:willUnmount() function App:willUnmount()
self.waypointConnection:Disconnect() self.waypointConnection:Disconnect()
self.confirmationBindable:Destroy() self.confirmationBindable:Destroy()
self.disconnectUpdatesCheckChanged()
self.disconnectPrereleasesCheckChanged()
self.autoConnectPlaytestServerListener() self.autoConnectPlaytestServerListener()
self:clearRunningConnectionInfo() self:clearRunningConnectionInfo()
end end
@@ -225,6 +240,40 @@ function App:closeNotification(id: number)
}) })
end 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() function App:getPriorEndpoint()
local priorEndpoints = Settings:get("priorEndpoints") local priorEndpoints = Settings:get("priorEndpoints")
if not priorEndpoints then if not priorEndpoints then

View File

@@ -14,6 +14,8 @@ local defaultSettings = {
twoWaySync = false, twoWaySync = false,
showNotifications = true, showNotifications = true,
syncReminder = true, syncReminder = true,
checkForUpdates = true,
checkForPrereleases = false,
autoConnectPlaytestServer = false, autoConnectPlaytestServer = false,
confirmationBehavior = "Initial", confirmationBehavior = "Initial",
largeChangesConfirmationThreshold = 5, largeChangesConfirmationThreshold = 5,

View File

@@ -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) local function compare(a, b)
if a > b then if a > b then
return 1 return 1
@@ -30,7 +34,48 @@ function Version.compare(a, b)
return minor return minor
end 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 end
function Version.display(version) function Version.display(version)
@@ -43,4 +88,64 @@ function Version.display(version)
return output return output
end 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 return Version

View File

@@ -3,6 +3,7 @@ return function()
it("should compare equal versions", 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 }, { 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, 4, 0 }, { 0, 4 })).to.equal(0)
expect(Version.compare({ 0, 0, 123 }, { 0, 0, 123 })).to.equal(0) expect(Version.compare({ 0, 0, 123 }, { 0, 0, 123 })).to.equal(0)
expect(Version.compare({ 26 }, { 26 })).to.equal(0) expect(Version.compare({ 26 }, { 26 })).to.equal(0)
@@ -13,6 +14,7 @@ return function()
it("should compare newer, older versions", function() it("should compare newer, older versions", function()
expect(Version.compare({ 1 }, { 0 })).to.equal(1) expect(Version.compare({ 1 }, { 0 })).to.equal(1)
expect(Version.compare({ 1, 1 }, { 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) end)
it("should compare different major versions", function() 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({ 1, 2, 3 }, { 1, 3, 2 })).to.equal(-1)
expect(Version.compare({ 50, 1 }, { 50, 2 })).to.equal(-1) expect(Version.compare({ 50, 1 }, { 50, 2 })).to.equal(-1)
end) 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 end

31
plugin/src/timeUtil.lua Normal file
View File

@@ -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