mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-23 22:25:26 +00:00
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. 
This commit is contained in:
@@ -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,
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user