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])
* 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

View File

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

View File

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

View File

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

View File

@@ -14,6 +14,8 @@ local defaultSettings = {
twoWaySync = false,
showNotifications = true,
syncReminder = true,
checkForUpdates = true,
checkForPrereleases = false,
autoConnectPlaytestServer = false,
confirmationBehavior = "Initial",
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)
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

View File

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

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