Skip to content

Commit

Permalink
Add client modes
Browse files Browse the repository at this point in the history
Signed-off-by: Axel Boberg <[email protected]>
  • Loading branch information
axelboberg committed Mar 9, 2024
1 parent db0bb79 commit 1994eee
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 2 deletions.
96 changes: 95 additions & 1 deletion api/browser/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,30 @@
// SPDX-License-Identifier: MIT

const MissingIdentityError = require('../error/MissingIdentityError')
const InvalidArgumentError = require('../error/InvalidArgumentError')
const state = require('../state')

const LazyValue = require('../classes/LazyValue')

/**
* @typedef {{
* id: String,
* role: Number,
* heartbeat: Number,
* isPersistent: Boolean,
* isEditingLayout: Boolean
* }} Connection
*/

/**
* Roles that a
* client can assume
*/
const ROLES = {
satellite: 0,
main: 1
}

/**
* The client's
* current identity
Expand Down Expand Up @@ -55,6 +75,7 @@ function assertIdentity () {
}

/**
* @private
* Ensure that a 'thing' is an array,
* if it's not, one will be created and
* the 'thing' will be inserted
Expand Down Expand Up @@ -197,7 +218,77 @@ async function getSelection () {
return (await state.get(`_connections.${getIdentity()}.selection`)) || []
}

/**
* Get all clients
* from the state
* @returns { Promise.<Connection[]> }
*/
async function getAllConnections () {
return Object.entries((await state.get('_connections')) || {})
.map(([id, connection]) => ({
id,
...connection,
role: (connection.role == null ? ROLES.satellite : connection.role)
}))
}

/**
* Set the role of a
* client by its id
* @param { String } id
* @param { Number } role
*/
async function setRole (id, role) {
if (!id || typeof id !== 'string') {
throw new InvalidArgumentError('Invalid argument \'id\', must be a string')
}

if (!Object.values(ROLES).includes(role)) {
throw new InvalidArgumentError('Invalid argument \'role\', must be a valid role')
}

const set = {
_connections: {
[id]: {
role
}
}
}

/*
There can only be one client with the main role,
if set, demote all other mains to satellite
*/
if (role === ROLES.main) {
(await getConnectionsByRole(ROLES.main))
/*
Don't reset the role of the
connection we're currently setting
*/
.filter(connection => connection.id !== id)
.forEach(connection => { set._connections[connection.id] = { role: ROLES.satellite } })
}

state.apply(set)
}

/**
* Get an array of all clients that
* have assumed a certain role
* @param { Number } role A valid role
* @returns { Promise.<Connection[]> }
*/
async function getConnectionsByRole (role) {
if (!Object.values(ROLES).includes(role)) {
throw new InvalidArgumentError('Invalid argument \'role\', must be a valid role')
}

return (await getAllConnections())
.filter(connection => connection.role === role)
}

module.exports = {
roles: ROLES,
setIdentity,
getIdentity,
awaitIdentity,
Expand All @@ -206,5 +297,8 @@ module.exports = {
addSelection,
subtractSelection,
clearSelection,
isSelected
isSelected,
setRole,
getAllConnections,
getConnectionsByRole
}
9 changes: 9 additions & 0 deletions app/components/Header/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from 'react'
import { SharedContext } from '../../sharedContext'
import { LocalContext } from '../../localContext'

import { Role } from '../Role'
import { Modal } from '../Modal'
import { Sharing } from '../Sharing'
import { Preferences } from '../Preferences'
Expand All @@ -27,9 +28,11 @@ export function Header ({ title = 'Bridge' }) {

const [sharingOpen, setSharingOpen] = React.useState(false)
const [prefsOpen, setPrefsOpen] = React.useState(false)
const [roleOpen, setRoleOpen] = React.useState(false)

const connectionCount = Object.keys(shared?._connections || {}).length
const isEditingLayout = shared?._connections?.[local?.id]?.isEditingLayout
const role = shared?._connections?.[local.id]?.role

/**
* Set the `isEditingLayout` toggle on
Expand Down Expand Up @@ -65,6 +68,12 @@ export function Header ({ title = 'Bridge' }) {
</div>
<div className='Header-center'></div>
<div className='Header-block'>
<div className='Header-actionSection'>
<button className={`Header-button Header-roleBtn ${role === 1 ? 'is-main' : ''}`} onClick={() => setRoleOpen(true)}>
{role === 1 ? 'Main' : 'Satellite'}
</button>
<Role currentRole={role} open={roleOpen} onClose={() => setRoleOpen(false)} />
</div>
<div className='Header-actionSection'>
<button className='Header-button Header-sharingBtn' onClick={() => setSharingOpen(true)}>
<Icon name='person' />
Expand Down
27 changes: 27 additions & 0 deletions app/components/Header/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,30 @@ for the traffic light
font-family: var(--base-fontFamily--primary);
}

.Header-button.Header-roleBtn {
width: auto;
padding: 0 7px 0 0;

border: 1px solid var(--base-color--shade);

white-space: nowrap;
overflow: hidden;
}

.Header-button.Header-roleBtn::before {
content: '';
height: 100%;
width: 6px;

margin-right: 7px;

background: var(--base-color--shade);
}

.Header-button.Header-roleBtn.is-main::before {
background: var(--base-color--accent1);
}

.Header-button {
display: flex;
position: relative;
Expand All @@ -58,7 +82,10 @@ for the traffic light
margin-left: 7px;
border: none;

color: inherit;
font-size: 1em;
font-family: var(--base-fontFamily--primary);

background: none;

align-items: center;
Expand Down
46 changes: 46 additions & 0 deletions app/components/Role/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from 'react'

import * as api from '../../api'

import { Popover } from '../Popover'
import { PopupConfirm } from '../Popup/confirm'

import './style.css'

export function Role ({ currentRole = 0, open, onClose = () => {} }) {
const [popupIsOpen, setPopupIsOpen] = React.useState()

async function handleAssumeMain (value) {
if (value) {
const bridge = await api.load()
const id = bridge.client.getIdentity()
bridge.client.setRole(id, bridge.client.roles.main)
}
setPopupIsOpen(false)
}

return (
<>
<PopupConfirm open={popupIsOpen} confirmText='Become main' abortText='Cancel' onChange={value => handleAssumeMain(value)}>
<div className='u-heading--2'>Become main</div>
This will turn the current<br />main client into satellite mode
</PopupConfirm>
<Popover open={open} onClose={onClose}>
<div className='Role u-theme--light'>
<div className='Role-content'>
Only the main client's selections can be triggered by the api.
{
currentRole === 1
? <div className='Role-status'>This is the main client.</div>
: (
<button className='Button Button--secondary u-width--100pct Sharing-copyBtn' onClick={() => setPopupIsOpen(true)}>
Become main
</button>
)
}
</div>
</div>
</Popover>
</>
)
}
21 changes: 21 additions & 0 deletions app/components/Role/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.Role {
width: 300px;

text-align: left;
font-size: 1em;
color: var(--base-color);
}

.Role-content {
padding: 10px;
box-sizing: border-box;
}

.Role-info {
margin: 5px;
}

.Role-status {
margin-top: 10px;
opacity: 0.7;
}
2 changes: 1 addition & 1 deletion app/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ html, body, #root {

.u-width--100pct {
width: 100%;
}
}

0 comments on commit 1994eee

Please sign in to comment.