Skip to content
This repository has been archived by the owner on Dec 15, 2022. It is now read-only.

Add status bar indicator when sharing or joining a portal #15

Merged
merged 8 commits into from
Jul 11, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module.exports = new RealTimePackage({
workspace: atom.workspace,
notificationManager: atom.notifications,
commandRegistry: atom.commands,
tooltipManager: atom.tooltips,
clipboard: atom.clipboard,
pusherKey: process.env.REAL_TIME_PUSHER_KEY || PRODUCTION_REAL_TIME_PUSHER_KEY,
baseURL: process.env.REAL_TIME_BASE_URL || PRODUCTION_REAL_TIME_BASE_URL
Expand Down
16 changes: 13 additions & 3 deletions lib/guest-portal-binding.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@ const GuestPortalBinding = require('./guest-portal-binding')

module.exports =
class GuestPortalBinding {
constructor ({workspace, notificationManager}) {
constructor ({portal, workspace, notificationManager, hostDidDisconnect}) {
this.portal = portal
this.workspace = workspace
this.notificationManager = notificationManager
this.emitHostDidDisconnect = hostDidDisconnect
this.activePaneItem = null
this.activeEditor = null
this.activeEditorBinding = null
this.activeSharedEditor = null
this.monkeyPatchesByEditor = new WeakMap()
}

async setActiveSharedEditor (sharedEditor) {
async setActiveSharedEditor (sharedEditor) {
if (sharedEditor == null) {
await this.replaceActivePaneItem(new EmptyPortalPaneItem())
} else {
Expand Down Expand Up @@ -101,13 +103,16 @@ class GuestPortalBinding {
this.activeBufferBinding = null
}

this.emitHostDidDisconnect()
this.notificationManager.addInfo('Portal closed', {
description: 'Your host stopped sharing their editor.',
dismissable: true
})
}

async replaceActivePaneItem (newActivePaneItem) {
this.newActivePaneItem = newActivePaneItem

if (this.activePaneItem) {
const pane = this.workspace.paneForItem(this.activePaneItem)
const index = pane.getItems().indexOf(this.activePaneItem)
Expand All @@ -117,7 +122,12 @@ class GuestPortalBinding {
await this.workspace.open(newActivePaneItem)
}

this.activePaneItem = newActivePaneItem
this.activePaneItem = this.newActivePaneItem
this.newActivePaneItem = null
}

getActivePaneItem () {
return this.newActivePaneItem ? this.newActivePaneItem : this.activePaneItem
}

disposeActivePaneItem () {
Expand Down
28 changes: 28 additions & 0 deletions lib/portal-status-bar-indicator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module.exports =
class PortalStatusBarIndicator {
constructor ({clipboard, tooltipManager, portal}) {
this.focused = false
this.element = document.createElement('a')
this.element.classList.add('realtime-PortalStatusBarIndicator', 'inline-block')
this.element.onclick = () => clipboard.write(portal.id)
this.tooltip = tooltipManager.add(
this.element,
{title: 'Click to copy the portal ID to your clipboard'}
)
}

dispose () {
this.element.onclick = null
this.tooltip.dispose()
}

setFocused (focused) {
if (focused && !this.focused) {
this.element.classList.add('focused')
} else if (!focused && this.focused) {
this.element.classList.remove('focused')
}

this.focused = focused
}
}
50 changes: 47 additions & 3 deletions lib/real-time-package.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,21 @@ const BufferBinding = require('./buffer-binding')
const EditorBinding = require('./editor-binding')
const GuestPortalBinding = require('./guest-portal-binding')
const JoinPortalDialog = require('./join-portal-dialog')
const PortalStatusBarIndicator = require('./portal-status-bar-indicator')

module.exports =
class RealTimePackage {
constructor (options) {
const {
workspace, notificationManager, commandRegistry, clipboard, restGateway,
pubSubGateway, pusherKey, baseURL, heartbeatIntervalInMilliseconds,
workspace, notificationManager, commandRegistry, tooltipManager, clipboard,
restGateway, pubSubGateway, pusherKey, baseURL, heartbeatIntervalInMilliseconds,
didCreateOrJoinPortal
} = options

this.workspace = workspace
this.notificationManager = notificationManager
this.commandRegistry = commandRegistry
this.tooltipManager = tooltipManager
this.clipboard = clipboard
this.restGateway = restGateway
this.pubSubGateway = pubSubGateway
Expand All @@ -31,6 +33,7 @@ class RealTimePackage {
this.didCreateOrJoinPortal = didCreateOrJoinPortal
this.guestPortalBindings = []
this.sharedEditorsByEditor = new WeakMap()
this.statusBarTilesByPortalId = new Map()
}

activate () {
Expand Down Expand Up @@ -84,6 +87,7 @@ class RealTimePackage {
await portal.setActiveSharedEditor(sharedEditor)
})

this.addStatusBarIndicatorForPortal(portal, {isHost: true})
this.clipboard.write(portal.id)
this.notificationManager.addSuccess('Your portal is open for business', {
description: "Invite people to collaborate with you using your portal ID above. It's already on your clipboard. 👌",
Expand All @@ -106,9 +110,12 @@ class RealTimePackage {

async joinPortal (portalId) {
const portal = await this.getClient().joinPortal(portalId)
this.addStatusBarIndicatorForPortal(portal, {isHost: false})
const portalBinding = new GuestPortalBinding({
portal,
workspace: this.workspace,
notificationManager: this.notificationManager
notificationManager: this.notificationManager,
hostDidDisconnect: () => this.removeStatusBarIndicatorForPortal(portal)
})
this.guestPortalBindings.push(portalBinding)
portal.setDelegate(portalBinding)
Expand All @@ -119,6 +126,43 @@ class RealTimePackage {
binding.setFollowHostCursor(!binding.isFollowingHostCursor())
}

consumeStatusBar (statusBar) {
this.statusBar = statusBar
this.workspace.observeActivePaneItem(this.didChangeActivePaneItem.bind(this))
}

didChangeActivePaneItem (paneItem) {
for (let i = 0; i < this.guestPortalBindings.length; i++) {
const portalBinding = this.guestPortalBindings[i]
const isFocused = (paneItem === portalBinding.getActivePaneItem())
const statusBarTile = this.statusBarTilesByPortalId.get(portalBinding.portal.id)
if (statusBarTile) statusBarTile.getItem().setFocused(isFocused)
}
}

addStatusBarIndicatorForPortal (portal, {isHost}) {
const PRIORITY_BETWEEN_BRANCH_NAME_AND_GRAMMAR = -40
if (this.statusBar) {
const indicator = new PortalStatusBarIndicator({
clipboard: this.clipboard,
tooltipManager: this.tooltipManager,
portal
})
if (isHost) indicator.setFocused(true)
const tile = this.statusBar.addRightTile({item: indicator, priority: PRIORITY_BETWEEN_BRANCH_NAME_AND_GRAMMAR})
this.statusBarTilesByPortalId.set(portal.id, tile)
}
}

removeStatusBarIndicatorForPortal (portal) {
const tile = this.statusBarTilesByPortalId.get(portal.id)
if (tile) {
tile.getItem().dispose()
tile.destroy()
this.statusBarTilesByPortalId.delete(portal.id)
}
}

getClient () {
if (!this.client) {
this.client = new Client({
Expand Down
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@
"@atom/real-time-client": "https://user-with-just-readonly-access-to-realtime-repos:924792417376b236ca11cc234bcc95d306dcd1cb@api.github.com/repos/atom/real-time-client/tarball/v0.7.12",
"loophole": "^1.1.0"
},
"consumedServices": {
"status-bar": {
"versions": {
"^1.0.0": "consumeStatusBar"
}
}
},
"engines": {
"atom": ">=1.19.0"
}
Expand Down
11 changes: 11 additions & 0 deletions styles/real-time.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@import "ui-variables";
@import "octicon-mixins";

.realtime-PortalStatusBarIndicator {
.octicon(radio-tower);
color: @text-color-subtle;

&.focused {
color: @text-color-success;
}
}
85 changes: 85 additions & 0 deletions test/real-time-package.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ const suite = global.describe
const test = global.it
const temp = require('temp').track()

// TODO: For tests that aren't directly related to heartbeat logic, replace
// usage of eviction via heartbeat with explicit closing of a portal.

suite('RealTimePackage', function () {
if (process.env.CI) this.timeout(process.env.TEST_TIMEOUT_IN_MS)

Expand Down Expand Up @@ -275,13 +278,74 @@ suite('RealTimePackage', function () {
await condition(() => guestEditor2.getScrollTopRow() === 3)
})

test('status bar indicator', async () => {
const HEARTBEAT_INTERVAL_IN_MS = 10
const EVICTION_PERIOD_IN_MS = 2 * HEARTBEAT_INTERVAL_IN_MS
testServer.heartbeatService.setEvictionPeriod(EVICTION_PERIOD_IN_MS)

const host1Env = buildAtomEnvironment()
const host1Package = buildPackage(host1Env, {heartbeatIntervalInMilliseconds: HEARTBEAT_INTERVAL_IN_MS})
const host1StatusBar = new FakeStatusBar()
host1Package.consumeStatusBar(host1StatusBar)

const host1Portal = await host1Package.sharePortal()
assert.equal(host1StatusBar.getRightTiles().length, 1)
assert(host1StatusBar.getRightTiles()[0].item.element.classList.contains('focused'))

host1Package.clipboard.write('')
host1StatusBar.getRightTiles()[0].item.element.click()
assert.equal(host1Package.clipboard.read(), host1Portal.id)

const host2Env = buildAtomEnvironment()
const host2Package = buildPackage(host2Env, {heartbeatIntervalInMilliseconds: HEARTBEAT_INTERVAL_IN_MS})
const host2Portal = await host2Package.sharePortal()

const guestEnv = buildAtomEnvironment()
const guestPackage = buildPackage(guestEnv, {heartbeatIntervalInMilliseconds: HEARTBEAT_INTERVAL_IN_MS})
const guestStatusBar = new FakeStatusBar()
guestPackage.consumeStatusBar(guestStatusBar)

await guestPackage.joinPortal(host1Portal.id)
await guestPackage.joinPortal(host2Portal.id)

assert.equal(guestStatusBar.getRightTiles().length, 2)
const [host1Tile, host2Tile] = guestStatusBar.getRightTiles()
assert(!host1Tile.item.element.classList.contains('focused'))
assert(host2Tile.item.element.classList.contains('focused'))

guestEnv.workspace.getActivePane().activateItemAtIndex(0)
assert(host1Tile.item.element.classList.contains('focused'))
assert(!host2Tile.item.element.classList.contains('focused'))

await guestEnv.workspace.open()
assert(!host1Tile.item.element.classList.contains('focused'))
assert(!host2Tile.item.element.classList.contains('focused'))

guestPackage.clipboard.write('')
host1Tile.item.element.click()
assert.equal(guestPackage.clipboard.read(), host1Portal.id)

guestPackage.clipboard.write('')
host2Tile.item.element.click()
assert.equal(guestPackage.clipboard.read(), host2Portal.id)

await host1Portal.simulateNetworkFailure()
await condition(async () => deepEqual(
await testServer.heartbeatService.findDeadSites(),
[{portalId: host1Portal.id, id: host1Portal.siteId}]
))
testServer.heartbeatService.evictDeadSites()
await condition(() => deepEqual(guestStatusBar.getRightTiles(), [host2Tile]))
})

function buildPackage (env, {heartbeatIntervalInMilliseconds} = {}) {
return new RealTimePackage({
restGateway: testServer.restGateway,
pubSubGateway: testServer.pubSubGateway,
workspace: env.workspace,
notificationManager: env.notifications,
commandRegistry: env.commands,
tooltipManager: env.tooltips,
clipboard: new FakeClipboard(),
heartbeatIntervalInMilliseconds,
didCreateOrJoinPortal: (portal) => portals.push(portal)
Expand Down Expand Up @@ -333,3 +397,24 @@ class FakeClipboard {
this.text = text
}
}

class FakeStatusBar {
constructor () {
this.rightTiles = []
}

getRightTiles () {
return this.rightTiles
}

addRightTile (tile) {
this.rightTiles.push(tile)
return {
getItem: () => tile.item,
destroy: () => {
const index = this.rightTiles.indexOf(tile)
this.rightTiles.splice(index, 1)
}
}
}
}