Fix the diff visualizer of connected sessions (#674)

Clicking on the "X changes X ago" message opens up a handy diff
visualizer to see what those changes were. However, it had quite a few
issues that needed fixing.

- Disconnecting a session with it expanded caused an error as it tried
to read the serveSession that no longer exists during the page fade
transition. (#671)
- Resolved by converting to stateful component and holding the
serveSession during the lifetime to ensure it can render the last known
changes during the fade transition
- Leaving it open while new changes are synced did not update the
visualizer
- The patch data was piggybacking on an existing binding, which meant
that new patches did not trigger rerender.
    - Resolved by converting to state
    - Also made some improvements to that old binding
- Moved from app to connected page for better organization and
separation of duties
      - No more useless updates causing rerenders with no real change
- Scroll window child component wouldn't actually display the updated
visuals
    - Resolved by making major improvements to VirtualScroller
      - Made more robust against edge case states
      - Made smarter about knowing when it needs to refresh

As you can see in this slow motion GIF, it works now.

![slowedDemo](https://github.com/rojo-rbx/rojo/assets/40185666/c9fc8489-72a9-47be-ae45-9c420e1535d4)
This commit is contained in:
boatbomber
2023-06-04 01:46:16 -04:00
committed by GitHub
parent 6b0f7f94b6
commit 6542304340
4 changed files with 98 additions and 56 deletions

View File

@@ -40,11 +40,6 @@ end
local DomLabel = Roact.Component:extend("DomLabel") local DomLabel = Roact.Component:extend("DomLabel")
function DomLabel:init() function DomLabel:init()
self.maxElementHeight = 0
if self.props.changeList then
self.maxElementHeight = math.clamp(#self.props.changeList * 30, 30, 30 * 6)
end
local initHeight = self.props.elementHeight:getValue() local initHeight = self.props.elementHeight:getValue()
self.expanded = initHeight > 30 self.expanded = initHeight > 30
@@ -118,7 +113,8 @@ function DomLabel:render()
Size = UDim2.new(1, 0, 1, 0), Size = UDim2.new(1, 0, 1, 0),
[Roact.Event.Activated] = function() [Roact.Event.Activated] = function()
self.expanded = not self.expanded self.expanded = not self.expanded
self.motor:setGoal(Flipper.Spring.new((self.expanded and self.maxElementHeight or 0) + 30, { local goalHeight = 30 + (if self.expanded then math.clamp(#self.props.changeList * 30, 30, 30 * 6) else 0)
self.motor:setGoal(Flipper.Spring.new(goalHeight, {
frequency = 5, frequency = 5,
dampingRatio = 1, dampingRatio = 1,
})) }))

View File

@@ -99,18 +99,30 @@ function VirtualScroller:refresh()
}) })
end end
function VirtualScroller:didUpdate(previousProps)
if self.props.count ~= previousProps.count then
-- Items have changed, so we need to refresh
self:refresh()
end
end
function VirtualScroller:render() function VirtualScroller:render()
local props, state = self.props, self.state local props, state = self.props, self.state
local items = {} local items = {}
for i = state.Start, state.End do for i = state.Start, state.End do
local content = props.render(i)
if content == nil then
continue
end
items["Item" .. i] = e("Frame", { items["Item" .. i] = e("Frame", {
LayoutOrder = i, LayoutOrder = i,
Size = props.getHeightBinding(i):map(function(height) Size = props.getHeightBinding(i):map(function(height)
return UDim2.new(1, 0, 0, height) return UDim2.new(1, 0, 0, height)
end), end),
BackgroundTransparency = 1, BackgroundTransparency = 1,
}, props.render(i)) }, content)
end end
return Theme.with(function(theme) return Theme.with(function(theme)

View File

@@ -26,11 +26,11 @@ function timeSinceText(elapsed: number): string
local ageText = string.format("%d seconds ago", elapsed) local ageText = string.format("%d seconds ago", elapsed)
for _,UnitData in ipairs(AGE_UNITS) do for _, UnitData in ipairs(AGE_UNITS) do
local UnitSeconds, UnitName = UnitData[1], UnitData[2] local UnitSeconds, UnitName = UnitData[1], UnitData[2]
if elapsed > UnitSeconds then if elapsed > UnitSeconds then
local c = math.floor(elapsed/UnitSeconds) local c = math.floor(elapsed / UnitSeconds)
ageText = string.format("%d %s%s ago", c, UnitName, c>1 and "s" or "") ageText = string.format("%d %s%s ago", c, UnitName, c > 1 and "s" or "")
break break
end end
end end
@@ -38,45 +38,53 @@ function timeSinceText(elapsed: number): string
return ageText return ageText
end end
local function ChangesDrawer(props) local ChangesDrawer = Roact.Component:extend("ConnectedPage")
if props.rendered == false then
function ChangesDrawer:init()
-- Hold onto the serve session during the lifecycle of this component
-- so that it can still render during the fade out after disconnecting
self.serveSession = self.props.serveSession
end
function ChangesDrawer:render()
if self.props.rendered == false or self.serveSession == nil then
return nil return nil
end end
return Theme.with(function(theme) return Theme.with(function(theme)
return e(BorderedContainer, { return e(BorderedContainer, {
transparency = props.transparency, transparency = self.props.transparency,
size = props.height:map(function(y) size = self.props.height:map(function(y)
return UDim2.new(1, 0, y, -180 * y) return UDim2.new(1, 0, y, -180 * y)
end), end),
position = UDim2.new(0, 0, 1, 0), position = UDim2.new(0, 0, 1, 0),
anchorPoint = Vector2.new(0, 1), anchorPoint = Vector2.new(0, 1),
layoutOrder = props.layoutOrder, layoutOrder = self.props.layoutOrder,
}, { }, {
Close = e(IconButton, { Close = e(IconButton, {
icon = Assets.Images.Icons.Close, icon = Assets.Images.Icons.Close,
iconSize = 24, iconSize = 24,
color = theme.ConnectionDetails.DisconnectColor, color = theme.ConnectionDetails.DisconnectColor,
transparency = props.transparency, transparency = self.props.transparency,
position = UDim2.new(1, 0, 0, 0), position = UDim2.new(1, 0, 0, 0),
anchorPoint = Vector2.new(1, 0), anchorPoint = Vector2.new(1, 0),
onClick = props.onClose, onClick = self.props.onClose,
}, { }, {
Tip = e(Tooltip.Trigger, { Tip = e(Tooltip.Trigger, {
text = "Close the patch visualizer" text = "Close the patch visualizer",
}), }),
}), }),
PatchVisualizer = e(PatchVisualizer, { PatchVisualizer = e(PatchVisualizer, {
size = UDim2.new(1, 0, 1, 0), size = UDim2.new(1, 0, 1, 0),
transparency = props.transparency, transparency = self.props.transparency,
layoutOrder = 3, layoutOrder = 3,
columnVisibility = {true, false, true}, columnVisibility = { true, false, true },
patch = props.patchInfo:getValue().patch, patch = self.props.patch,
instanceMap = props.serveSession.__instanceMap, instanceMap = self.serveSession.__instanceMap,
}), }),
}) })
end) end)
@@ -91,7 +99,7 @@ local function ConnectionDetails(props)
}, { }, {
TextContainer = e("Frame", { TextContainer = e("Frame", {
Size = UDim2.new(1, 0, 1, 0), Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1 BackgroundTransparency = 1,
}, { }, {
ProjectName = e("TextLabel", { ProjectName = e("TextLabel", {
Text = props.projectName, Text = props.projectName,
@@ -141,7 +149,7 @@ local function ConnectionDetails(props)
onClick = props.onDisconnect, onClick = props.onDisconnect,
}, { }, {
Tip = e(Tooltip.Trigger, { Tip = e(Tooltip.Trigger, {
text = "Disconnect from the Rojo sync server" text = "Disconnect from the Rojo sync server",
}), }),
}), }),
@@ -155,6 +163,18 @@ end
local ConnectedPage = Roact.Component:extend("ConnectedPage") local ConnectedPage = Roact.Component:extend("ConnectedPage")
function ConnectedPage:getChangeInfoText()
local patchData = self.props.patchData
if patchData == nil then
return ""
end
local elapsed = os.time() - patchData.timestamp
local changes = PatchSet.countChanges(patchData.patch)
return string.format("<i>Synced %d change%s %s</i>", changes, changes == 1 and "" or "s", timeSinceText(elapsed))
end
function ConnectedPage:init() function ConnectedPage:init()
self.changeDrawerMotor = Flipper.SingleMotor.new(0) self.changeDrawerMotor = Flipper.SingleMotor.new(0)
self.changeDrawerHeight = bindingUtil.fromMotor(self.changeDrawerMotor) self.changeDrawerHeight = bindingUtil.fromMotor(self.changeDrawerMotor)
@@ -176,6 +196,34 @@ function ConnectedPage:init()
self:setState({ self:setState({
renderChanges = false, renderChanges = false,
}) })
self.changeInfoText, self.setChangeInfoText = Roact.createBinding("")
self.changeInfoTextUpdater = task.defer(function()
while true do
self.setChangeInfoText(self:getChangeInfoText())
local elapsed = os.time() - self.props.patchData.timestamp
local updateInterval = 1
-- Update timestamp text as frequently as currently needed
for _, UnitData in ipairs(AGE_UNITS) do
local UnitSeconds = UnitData[1]
if elapsed >= UnitSeconds then
updateInterval = UnitSeconds
break
end
end
task.wait(updateInterval)
end
end)
end
function ConnectedPage:willUnmount()
if self.changeInfoTextUpdater then
task.cancel(self.changeInfoTextUpdater)
end
end end
function ConnectedPage:render() function ConnectedPage:render()
@@ -208,15 +256,7 @@ function ConnectedPage:render()
}), }),
ChangeInfo = e("TextButton", { ChangeInfo = e("TextButton", {
Text = self.props.patchInfo:map(function(info) Text = self.changeInfoText,
local changes = PatchSet.countChanges(info.patch)
return string.format(
"<i>Synced %d change%s %s</i>",
changes,
changes == 1 and "" or "s",
timeSinceText(os.time() - info.timestamp)
)
end),
Font = Enum.Font.Gotham, Font = Enum.Font.Gotham,
TextSize = 14, TextSize = 14,
TextWrapped = true, TextWrapped = true,
@@ -249,7 +289,7 @@ function ConnectedPage:render()
ChangesDrawer = e(ChangesDrawer, { ChangesDrawer = e(ChangesDrawer, {
rendered = self.state.renderChanges, rendered = self.state.renderChanges,
transparency = self.props.transparency, transparency = self.props.transparency,
patchInfo = self.props.patchInfo, patch = self.props.patchData.patch,
serveSession = self.props.serveSession, serveSession = self.props.serveSession,
height = self.changeDrawerHeight, height = self.changeDrawerHeight,
layoutOrder = 4, layoutOrder = 4,

View File

@@ -51,10 +51,6 @@ function App:init()
self.host, self.setHost = Roact.createBinding(priorHost or "") self.host, self.setHost = Roact.createBinding(priorHost or "")
self.port, self.setPort = Roact.createBinding(priorPort or "") self.port, self.setPort = Roact.createBinding(priorPort or "")
self.patchInfo, self.setPatchInfo = Roact.createBinding({
patch = PatchSet.newEmpty(),
timestamp = os.time(),
})
self.confirmationBindable = Instance.new("BindableEvent") self.confirmationBindable = Instance.new("BindableEvent")
self.confirmationEvent = self.confirmationBindable.Event self.confirmationEvent = self.confirmationBindable.Event
@@ -62,6 +58,10 @@ function App:init()
appStatus = AppStatus.NotConnected, appStatus = AppStatus.NotConnected,
guiEnabled = false, guiEnabled = false,
confirmData = {}, confirmData = {},
patchData = {
patch = PatchSet.newEmpty(),
timestamp = os.time(),
},
notifications = {}, notifications = {},
toolbarIcon = Assets.Images.PluginButton, toolbarIcon = Assets.Images.PluginButton,
}) })
@@ -227,21 +227,25 @@ function App:startSession()
local now = os.time() local now = os.time()
local old = self.patchInfo:getValue() local old = self.state.patchData
if now - old.timestamp < 2 then if now - old.timestamp < 2 then
-- Patches that apply in the same second are -- Patches that apply in the same second are
-- considered to be part of the same change for human clarity -- considered to be part of the same change for human clarity
local merged = PatchSet.newEmpty() local merged = PatchSet.newEmpty()
PatchSet.assign(merged, old.patch, patch) PatchSet.assign(merged, old.patch, patch)
self.setPatchInfo({ self:setState({
patch = merged, patchData = {
timestamp = now, patch = merged,
timestamp = now,
},
}) })
else else
self.setPatchInfo({ self:setState({
patch = patch, patchData = {
timestamp = now, patch = patch,
timestamp = now,
},
}) })
end end
end) end)
@@ -318,16 +322,6 @@ function App:startSession()
serveSession:start() serveSession:start()
self.serveSession = serveSession self.serveSession = serveSession
task.defer(function()
while self.serveSession == serveSession do
-- Trigger rerender to update timestamp text
local patchInfo = table.clone(self.patchInfo:getValue())
self.setPatchInfo(patchInfo)
local elapsed = os.time() - patchInfo.timestamp
task.wait(elapsed < 60 and 1 or elapsed / 5)
end
end)
end end
function App:endSession() function App:endSession()
@@ -429,7 +423,7 @@ function App:render()
Connected = createPageElement(AppStatus.Connected, { Connected = createPageElement(AppStatus.Connected, {
projectName = self.state.projectName, projectName = self.state.projectName,
address = self.state.address, address = self.state.address,
patchInfo = self.patchInfo, patchData = self.state.patchData,
serveSession = self.serveSession, serveSession = self.serveSession,
onDisconnect = function() onDisconnect = function()