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

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