diff --git a/ui/src/agent-connection-status-icon.ts b/ui/src/agent-connection-status-icon.ts index 8f801d7..fd27f60 100644 --- a/ui/src/agent-connection-status-icon.ts +++ b/ui/src/agent-connection-status-icon.ts @@ -17,7 +17,7 @@ import { Profile, } from '@holochain-open-dev/profiles'; import { EntryRecord } from '@holochain-open-dev/utils'; -import { ConnectionStatus } from './room-view'; +import { ConnectionStatus } from './streams-store'; import { connectionStatusToColor } from './utils'; import { sharedStyles } from './sharedStyles'; import './holo-identicon'; @@ -102,6 +102,8 @@ export class AgentConnectionStatusIcon extends LitElement { }...`; case 'SdpExchange': return 'exchanging SDP data...'; + case "Blocked": + return "Blocked"; default: return 'unknown status type'; } diff --git a/ui/src/agent-connection-status.ts b/ui/src/agent-connection-status.ts index 4079463..c6bcc81 100644 --- a/ui/src/agent-connection-status.ts +++ b/ui/src/agent-connection-status.ts @@ -1,10 +1,15 @@ import { consume } from '@lit/context'; -import { hashProperty, sharedStyles } from '@holochain-open-dev/elements'; +import { + hashProperty, + sharedStyles, + wrapPathInSvg, +} from '@holochain-open-dev/elements'; import { css, html, LitElement, PropertyValueMap } from 'lit'; -import { property, customElement } from 'lit/decorators.js'; +import { property, customElement, state } from 'lit/decorators.js'; import { AgentPubKey, encodeHashToBase64 } from '@holochain/client'; import { localized, msg } from '@lit/localize'; import { StoreSubscriber } from '@holochain-open-dev/stores'; +import { mdiCancel } from '@mdi/js'; import '@holochain-open-dev/elements/dist/elements/display-error.js'; import '@shoelace-style/shoelace/dist/components/avatar/avatar.js'; @@ -17,14 +22,17 @@ import { Profile, } from '@holochain-open-dev/profiles'; import { EntryRecord } from '@holochain-open-dev/utils'; -import { ConnectionStatus } from './room-view'; +import { ConnectionStatus, StreamsStore } from './streams-store'; import { connectionStatusToColor } from './utils'; import './holo-identicon'; +import { streamsStoreContext } from './contexts'; @localized() @customElement('agent-connection-status') export class AgentConnectionStatus extends LitElement { - /** Public properties */ + @consume({ context: streamsStoreContext, subscribe: true }) + @state() + streamsStore!: StreamsStore; /** * REQUIRED. The public key identifying the agent whose profile is going to be shown. @@ -62,6 +70,13 @@ export class AgentConnectionStatus extends LitElement { () => [this.agentPubKey, this.store] ); + _isAgentBlocked = new StoreSubscriber( + this, + () => + this.streamsStore.isAgentBlocked(encodeHashToBase64(this.agentPubKey)), + () => [this.agentPubKey, this.streamsStore] + ); + async willUpdate( changedProperties: PropertyValueMap | Map ) { @@ -109,6 +124,8 @@ export class AgentConnectionStatus extends LitElement { }...`; case 'SdpExchange': return 'exchanging SDP data...'; + case 'Blocked': + return 'Blocked'; default: return 'unknown status type'; } @@ -117,42 +134,73 @@ export class AgentConnectionStatus extends LitElement { renderProfile(profile: EntryRecord | undefined) { return html`
- ${profile && profile.entry.fields.avatar - ? html` - ${profile.entry.nickname}'s avatar - ` - : html` - - - `} + + ${profile && profile.entry.fields.avatar + ? html` + ${profile.entry.nickname}'s avatar + ` + : html` + + + `} +
${profile ? profile.entry.nickname : 'Unknown'} ${this.statusToText(this.connectionStatus)}
+ + + { + if (this._isAgentBlocked.value) { + this.streamsStore.unblockAgent( + encodeHashToBase64(this.agentPubKey) + ); + } else { + this.streamsStore.blockAgent( + encodeHashToBase64(this.agentPubKey) + ); + } + }} + > +
`; } @@ -179,5 +227,44 @@ export class AgentConnectionStatus extends LitElement { } } - static styles = [sharedStyles, css``]; + static styles = [ + sharedStyles, + css` + sl-icon-button::part(base) { + color: #c72100; + } + sl-icon-button::part(base):hover, + sl-icon-button::part(base):focus { + color: #e35d42; + } + sl-icon-button::part(base):active { + color: #e35d42; + } + + .unblock { + background: 'green'; + } + + .unblock sl-icon-button::part(base) { + color: #09b500; + } + .unblock sl-icon-button::part(base):hover, + .unblock sl-icon-button::part(base):focus { + color: #39e430; + } + .unblock sl-icon-button::part(base):active { + color: #39e430; + } + + .tooltip-filled { + --sl-tooltip-background-color: #c3c9eb; + --sl-tooltip-arrow-size: 6px; + --sl-tooltip-border-radius: 5px; + --sl-tooltip-padding: 4px; + --sl-tooltip-font-size: 14px; + --sl-tooltip-color: #0d1543; + --sl-tooltip-font-family: 'Ubuntu', sans-serif; + } + `, + ]; } diff --git a/ui/src/room-view.ts b/ui/src/room-view.ts index 7a0405e..c884229 100644 --- a/ui/src/room-view.ts +++ b/ui/src/room-view.ts @@ -43,136 +43,12 @@ import './agent-connection-status'; import './agent-connection-status-icon'; import './toggle-switch'; import { sortConnectionStatuses } from './utils'; -import { StreamsStore } from './streams-store'; - -const ICE_CONFIG = [ - { urls: 'stun:global.stun.twilio.com:3478' }, - { urls: 'stun:stun.l.google.com:19302' }, -]; - -/** - * If an InitRequest does not succeed within this duration (ms) another InitRequest will be sent - */ -const INIT_RETRY_THRESHOLD = 5000; - -const PING_INTERVAL = 2000; - -type ConnectionId = string; - -type RTCMessage = - | { - type: 'action'; - message: 'video-off' | 'audio-off' | 'audio-on'; - } - | { - type: 'text'; - message: string; - }; - -type OpenConnectionInfo = { - connectionId: ConnectionId; - peer: SimplePeer.Instance; - video: boolean; - audio: boolean; - connected: boolean; - direction: 'outgoing' | 'incoming' | 'duplex'; // In which direction streams are expected -}; - -type PendingInit = { - /** - * UUID to identify the connection - */ - connectionId: ConnectionId; - /** - * Timestamp when init was sent. If InitAccept is not received within a certain duration - * after t0, a next InitRequest is sent. - */ - t0: number; -}; - -type PendingAccept = { - /** - * UUID to identify the connection - */ - connectionId: ConnectionId; - /** - * Peer instance that was created with this accept. Gets destroyed if another Peer object makes it through - * to connected state instead for a connection with the same Agent. - */ - peer: SimplePeer.Instance; -}; - -type PongMetaData = { - formatVersion: number; - data: T; -}; - -type PongMetaDataV1 = { - connectionStatuses: ConnectionStatuses; - screenShareConnectionStatuses?: ConnectionStatuses; - knownAgents?: Record; - appVersion?: string; -}; - -type ConnectionStatuses = Record; - -/** - * Connection status with a peer - */ -export type ConnectionStatus = - | { - /** - * No WebRTC connection or freshly disconnected - */ - type: 'Disconnected'; - } - | { - /** - * Waiting for an init of a peer whose pubkey is alphabetically higher than ours - */ - type: 'AwaitingInit'; - } - | { - /** - * Waiting for an Accept of a peer whose pubkey is alphabetically lower than ours - */ - type: 'InitSent'; - attemptCount?: number; - } - | { - /** - * Waiting for SDP exchange to start - */ - type: 'AcceptSent'; - attemptCount?: number; - } - | { - /** - * SDP exchange is ongoing - */ - type: 'SdpExchange'; - } - | { - /** - * WebRTC connection is established - */ - type: 'Connected'; - }; - -type AgentInfo = { - pubkey: AgentPubKeyB64; - /** - * If I know from the all_agents anchor that this agent exists in the Room, the - * type is "known". If I've learnt about this agent only from other's Pong meta data - * or from receiving a Pong from that agent themselves the type is "told". - */ - type: 'known' | 'told'; - /** - * last time when a PongUi from this agent was received - */ - lastSeen?: number; - appVersion?: string; -}; +import { + AgentInfo, + ConnectionStatuses, + PING_INTERVAL, + StreamsStore, +} from './streams-store'; @localized() @customElement('room-view') @@ -643,13 +519,17 @@ export class RoomView extends LitElement { const presentAgents = knownAgentsKeysB64 .filter(pubkeyB64 => { const status = this._connectionStatuses.value[pubkeyB64]; - return !!status && status.type !== 'Disconnected'; + return ( + !!status && + status.type !== 'Disconnected' && + status.type !== 'Blocked' + ); }) .sort((key_a, key_b) => key_a.localeCompare(key_b)); const absentAgents = knownAgentsKeysB64 .filter(pubkeyB64 => { const status = this._connectionStatuses.value[pubkeyB64]; - return !status || status.type === 'Disconnected'; + return !status || status.type === 'Disconnected' || status.type === "Blocked"; }) .sort((key_a, key_b) => key_a.localeCompare(key_b)); return html` @@ -667,6 +547,7 @@ export class RoomView extends LitElement { pubkey => pubkey, pubkey => html` pubkey, pubkey => html` diff --git a/ui/src/sharedStyles.ts b/ui/src/sharedStyles.ts index 6e7af15..407e88b 100644 --- a/ui/src/sharedStyles.ts +++ b/ui/src/sharedStyles.ts @@ -13,6 +13,14 @@ export const sharedStyles = css` font-family: 'Ubuntu', sans-serif; } + .flex-1 { + flex: 1; + } + + .flex { + display: flex; + } + .column { display: flex; flex-direction: column; diff --git a/ui/src/streams-store.ts b/ui/src/streams-store.ts index 719b816..65ebc10 100644 --- a/ui/src/streams-store.ts +++ b/ui/src/streams-store.ts @@ -5,7 +5,13 @@ import { decodeHashFromBase64, encodeHashToBase64, } from '@holochain/client'; -import { get, writable, Writable } from '@holochain-open-dev/stores'; +import { + derived, + get, + Readable, + writable, + Writable, +} from '@holochain-open-dev/stores'; import { v4 as uuidv4 } from 'uuid'; import { RoomSignal } from './types'; import { RoomClient } from './room-client'; @@ -39,7 +45,7 @@ const ICE_CONFIG = [ * */ -type StoreEventPayload = +export type StoreEventPayload = | { type: 'my-video-on'; } @@ -126,7 +132,7 @@ type StoreEventPayload = */ const INIT_RETRY_THRESHOLD = 5000; -const PING_INTERVAL = 2000; +export const PING_INTERVAL = 2000; type ConnectionId = string; @@ -185,7 +191,7 @@ type PongMetaDataV1 = { appVersion?: string; }; -type ConnectionStatuses = Record; +export type ConnectionStatuses = Record; /** * Connection status with a peer @@ -197,6 +203,12 @@ export type ConnectionStatus = */ type: 'Disconnected'; } + | { + /** + * Agent has been blocked by us + */ + type: 'Blocked'; + } | { /** * Waiting for an init of a peer whose pubkey is alphabetically higher than ours @@ -230,7 +242,7 @@ export type ConnectionStatus = type: 'Connected'; }; -type AgentInfo = { +export type AgentInfo = { pubkey: AgentPubKeyB64; /** * If I know from the all_agents anchor that this agent exists in the Room, the @@ -266,6 +278,8 @@ export class StreamsStore { private eventCallback: (ev: StoreEventPayload) => any = () => undefined; + blockedAgents: Writable = writable([]); + constructor( roomStore: RoomStore, screenSourceSelection: () => Promise @@ -280,6 +294,10 @@ export class StreamsStore { this.signalUnsubscribe = this.roomClient.onSignal(async signal => this.handleSignal(signal) ); + const blockedAgentsJson = window.sessionStorage.getItem('blockedAgents'); + this.blockedAgents.set( + blockedAgentsJson ? JSON.parse(blockedAgentsJson) : [] + ); } static async connect( @@ -361,9 +379,15 @@ export class StreamsStore { const connectionStatuses = currentValue; Object.keys(get(this._knownAgents)).forEach(agentB64 => { if (!connectionStatuses[agentB64]) { - connectionStatuses[agentB64] = { - type: 'Disconnected', - }; + if (get(this.blockedAgents).includes(agentB64)) { + connectionStatuses[agentB64] = { + type: 'Blocked', + }; + } else { + connectionStatuses[agentB64] = { + type: 'Disconnected', + }; + } } }); return connectionStatuses; @@ -371,9 +395,9 @@ export class StreamsStore { // Ping known agents // This could potentially be optimized by only pinging agents that are online according to Moss (which would only work in shared rooms though) - const agentsToPing = Object.keys(get(this._knownAgents)).map(pubkeyB64 => - decodeHashFromBase64(pubkeyB64) - ); + const agentsToPing = Object.keys(get(this._knownAgents)) + .filter(agent => !get(this.blockedAgents).includes(agent)) + .map(pubkeyB64 => decodeHashFromBase64(pubkeyB64)); await this.roomStore.client.pingFrontend(agentsToPing); } @@ -659,6 +683,52 @@ export class StreamsStore { if (relevantConnection) relevantConnection.peer.destroy(); } + blockAgent(pubKey64: AgentPubKeyB64) { + const currentlyBlockedAgents = get(this.blockedAgents); + if (!currentlyBlockedAgents.includes(pubKey64)) { + this.blockedAgents.set([...currentlyBlockedAgents, pubKey64]); + } + const blockedAgentsJson = window.sessionStorage.getItem('blockedAgents'); + const blockedAgents: AgentPubKeyB64[] = blockedAgentsJson + ? JSON.parse(blockedAgentsJson) + : []; + if (!blockedAgents.includes(pubKey64)) + window.sessionStorage.setItem( + 'blockedAgents', + JSON.stringify([...blockedAgents, pubKey64]) + ); + this.disconnectFromPeerVideo(pubKey64); + this.disconnectFromPeerScreen(pubKey64); + setTimeout(() => { + this._connectionStatuses.update(currentValue => { + const connectionStatuses = currentValue; + connectionStatuses[pubKey64] = { + type: 'Blocked', + }; + return connectionStatuses; + }); + }, 500); + } + + unblockAgent(pubKey64: AgentPubKeyB64) { + const currentlyBlockedAgents = get(this.blockedAgents); + this.blockedAgents.set( + currentlyBlockedAgents.filter(pubkey => pubkey !== pubKey64) + ); + const blockedAgentsJson = window.sessionStorage.getItem('blockedAgents'); + const blockedAgents: AgentPubKeyB64[] = blockedAgentsJson + ? JSON.parse(blockedAgentsJson) + : []; + window.sessionStorage.setItem( + 'blockedAgents', + JSON.stringify(blockedAgents.filter(pubkey => pubkey !== pubKey64)) + ); + } + + isAgentBlocked(pubKey64: AgentPubKeyB64): Readable { + return derived(this.blockedAgents, val => val.includes(pubKey64)); + } + // =========================================================================================== // WEBRTC STREAMS // =========================================================================================== @@ -1263,6 +1333,7 @@ export class StreamsStore { */ async handlePingUi(signal: Extract) { const pubkeyB64 = encodeHashToBase64(signal.from_agent); + if (get(this.blockedAgents).includes(pubkeyB64)) return; console.log(`Got PingUi from ${pubkeyB64}: `, signal); if (pubkeyB64 !== this.myPubKeyB64) { const metaData: PongMetaData = { diff --git a/ui/src/utils.ts b/ui/src/utils.ts index baa8a9f..b4af2a7 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -7,7 +7,7 @@ import { ProvisionedCell, RoleName, } from '@holochain/client'; -import { ConnectionStatus } from './room-view'; +import { ConnectionStatus } from './streams-store'; export type CellTypes = { provisioned: ProvisionedCell; @@ -101,6 +101,8 @@ export function connectionStatusToColor(status?: ConnectionStatus, offlineColor return 'yellow'; case 'Connected': return '#48e708'; + case "Blocked": + return '#c72100'; default: return offlineColor; }