2018-09-21 16:05:03 +02:00
class PeersUI {
constructor ( ) {
2023-10-31 18:32:23 +01:00
this . $xPeers = $$ ( 'x-peers' ) ;
this . $xNoPeers = $$ ( 'x-no-peers' ) ;
this . $xInstructions = $$ ( 'x-instructions' ) ;
2023-11-08 20:31:57 +01:00
this . $wsFallbackWarning = $ ( 'websocket-fallback' ) ;
2023-10-31 18:32:23 +01:00
2023-12-09 17:28:05 +01:00
this . $sharePanel = $$ ( '.shr-panel' ) ;
this . $shareModeImageThumb = $$ ( '.shr-panel .image-thumb' ) ;
this . $shareModeTextThumb = $$ ( '.shr-panel .text-thumb' ) ;
this . $shareModeFileThumb = $$ ( '.shr-panel .file-thumb' ) ;
this . $shareModeDescriptor = $$ ( '.shr-panel .share-descriptor' ) ;
this . $shareModeDescriptorItem = $$ ( '.shr-panel .descriptor-item' ) ;
this . $shareModeDescriptorOther = $$ ( '.shr-panel .descriptor-other' ) ;
this . $shareModeCancelBtn = $$ ( '.shr-panel .cancel-btn' ) ;
this . $shareModeEditBtn = $$ ( '.shr-panel .edit-btn' ) ;
2023-11-24 16:25:30 +01:00
2023-10-31 18:32:23 +01:00
this . peers = { } ;
2025-02-13 10:24:27 +01:00
this . shareMode = {
active : false ,
descriptor : "" ,
files : [ ] ,
text : ""
}
2023-11-08 20:31:57 +01:00
2018-09-21 16:05:03 +02:00
Events . on ( 'peer-joined' , e => this . _onPeerJoined ( e . detail ) ) ;
2023-11-24 16:25:30 +01:00
Events . on ( 'peer-added' , _ => this . _evaluateOverflowingPeers ( ) ) ;
2023-02-16 02:19:14 +01:00
Events . on ( 'peer-connected' , e => this . _onPeerConnected ( e . detail . peerId , e . detail . connectionHash ) ) ;
2022-12-22 22:41:26 +01:00
Events . on ( 'peer-disconnected' , e => this . _onPeerDisconnected ( e . detail ) ) ;
2018-09-21 16:05:03 +02:00
Events . on ( 'peers' , e => this . _onPeers ( e . detail ) ) ;
2023-01-17 10:41:50 +01:00
Events . on ( 'set-progress' , e => this . _onSetProgress ( e . detail ) ) ;
2023-10-31 18:32:23 +01:00
Events . on ( 'drop' , e => this . _onDrop ( e ) ) ;
Events . on ( 'keydown' , e => this . _onKeyDown ( e ) ) ;
2023-01-22 17:33:19 +01:00
Events . on ( 'dragover' , e => this . _onDragOver ( e ) ) ;
2023-01-22 16:14:27 +01:00
Events . on ( 'dragleave' , _ => this . _onDragEnd ( ) ) ;
Events . on ( 'dragend' , _ => this . _onDragEnd ( ) ) ;
2023-11-24 16:25:30 +01:00
Events . on ( 'resize' , _ => this . _evaluateOverflowingPeers ( ) ) ;
2023-12-12 13:39:55 +01:00
Events . on ( 'header-changed' , _ => this . _evaluateOverflowingPeers ( ) ) ;
2023-01-22 16:14:27 +01:00
2023-10-31 18:32:23 +01:00
Events . on ( 'paste' , e => this . _onPaste ( e ) ) ;
2023-11-24 16:25:30 +01:00
Events . on ( 'activate-share-mode' , e => this . _activateShareMode ( e . detail . files , e . detail . text ) ) ;
Events . on ( 'translation-loaded' , _ => this . _reloadShareMode ( ) ) ;
2023-10-31 18:32:23 +01:00
Events . on ( 'room-type-removed' , e => this . _onRoomTypeRemoved ( e . detail . peerId , e . detail . roomType ) ) ;
2023-03-01 21:35:00 +01:00
2023-08-30 14:57:40 +02:00
2023-11-24 16:25:30 +01:00
this . $shareModeCancelBtn . addEventListener ( 'click' , _ => this . _deactivateShareMode ( ) ) ;
2023-03-01 21:35:00 +01:00
2023-05-04 17:38:51 +02:00
Events . on ( 'peer-display-name-changed' , e => this . _onPeerDisplayNameChanged ( e ) ) ;
2023-03-01 21:35:00 +01:00
2023-11-08 20:31:57 +01:00
Events . on ( 'ws-config' , e => this . _evaluateRtcSupport ( e . detail ) )
2023-10-11 23:22:10 +02:00
}
2023-11-08 20:31:57 +01:00
_evaluateRtcSupport ( wsConfig ) {
if ( wsConfig . wsFallback ) {
this . $wsFallbackWarning . hidden = false ;
2023-03-01 21:35:00 +01:00
}
2023-11-08 20:31:57 +01:00
else {
this . $wsFallbackWarning . hidden = true ;
if ( ! window . isRtcSupported ) {
alert ( Localization . getTranslation ( "instructions.webrtc-requirement" ) ) ;
}
2023-03-01 21:35:00 +01:00
}
}
_changePeerDisplayName ( peerId , displayName ) {
this . peers [ peerId ] . name . displayName = displayName ;
const peerIdNode = $ ( peerId ) ;
if ( peerIdNode && displayName ) peerIdNode . querySelector ( '.name' ) . textContent = displayName ;
2023-05-04 17:38:51 +02:00
this . _redrawPeerRoomTypes ( peerId ) ;
}
_onPeerDisplayNameChanged ( e ) {
if ( ! e . detail . displayName ) return ;
this . _changePeerDisplayName ( e . detail . peerId , e . detail . displayName ) ;
2023-01-17 10:41:50 +01:00
}
2023-11-24 16:25:30 +01:00
async _onKeyDown ( e ) {
if ( ! this . shareMode . active || Dialog . anyDialogShown ( ) ) return ;
2023-11-02 02:56:01 +01:00
2023-11-24 16:25:30 +01:00
if ( e . key === "Escape" ) {
await this . _deactivateShareMode ( ) ;
2023-01-17 10:41:50 +01:00
}
2023-10-11 23:22:10 +02:00
// close About PairDrop page on Escape
if ( e . key === "Escape" ) {
window . location . hash = '#' ;
}
2018-09-21 16:05:03 +02:00
}
2023-01-10 05:07:57 +01:00
_onPeerJoined ( msg ) {
2023-09-13 18:15:01 +02:00
this . _joinPeer ( msg . peer , msg . roomType , msg . roomId ) ;
2023-01-10 05:07:57 +01:00
}
2023-09-13 18:15:01 +02:00
_joinPeer ( peer , roomType , roomId ) {
2023-05-04 17:38:51 +02:00
const existingPeer = this . peers [ peer . id ] ;
if ( existingPeer ) {
2023-09-13 18:15:01 +02:00
// peer already exists. Abort but add roomType to GUI
existingPeer . _roomIds [ roomType ] = roomId ;
this . _redrawPeerRoomTypes ( peer . id ) ;
2023-05-04 17:38:51 +02:00
return ;
}
2023-05-11 21:04:10 +02:00
2023-11-02 01:22:41 +01:00
peer . _isSameBrowser = ( ) => BrowserTabsConnector . peerIsSameBrowser ( peer . id ) ;
2023-09-13 18:15:01 +02:00
peer . _roomIds = { } ;
2023-05-11 21:04:10 +02:00
2023-09-13 18:15:01 +02:00
peer . _roomIds [ roomType ] = roomId ;
2022-12-22 22:41:26 +01:00
this . peers [ peer . id ] = peer ;
}
2023-02-16 02:19:14 +01:00
_onPeerConnected ( peerId , connectionHash ) {
2023-05-04 17:38:51 +02:00
if ( ! this . peers [ peerId ] || $ ( peerId ) ) return ;
const peer = this . peers [ peerId ] ;
2023-11-24 16:25:30 +01:00
new PeerUI ( peer , connectionHash , {
active : this . shareMode . active ,
descriptor : this . shareMode . descriptor ,
} ) ;
2018-09-21 16:05:03 +02:00
}
2023-05-04 17:38:51 +02:00
_redrawPeerRoomTypes ( peerId ) {
2023-09-13 18:15:01 +02:00
const peer = this . peers [ peerId ] ;
2023-05-04 17:38:51 +02:00
const peerNode = $ ( peerId ) ;
2023-09-13 18:15:01 +02:00
if ( ! peer || ! peerNode ) return ;
peerNode . classList . remove ( 'type-ip' , 'type-secret' , 'type-public-id' , 'type-same-browser' ) ;
if ( peer . _isSameBrowser ( ) ) {
peerNode . classList . add ( ` type-same-browser ` ) ;
2023-05-04 17:38:51 +02:00
}
2023-09-13 18:15:01 +02:00
Object . keys ( peer . _roomIds ) . forEach ( roomType => peerNode . classList . add ( ` type- ${ roomType } ` ) ) ;
2023-03-01 10:04:37 +01:00
}
2023-11-24 16:25:30 +01:00
_evaluateOverflowingPeers ( ) {
2023-03-01 10:04:37 +01:00
if ( this . $xPeers . clientHeight < this . $xPeers . scrollHeight ) {
this . $xPeers . classList . add ( 'overflowing' ) ;
2023-11-02 02:56:01 +01:00
}
else {
2023-03-01 10:04:37 +01:00
this . $xPeers . classList . remove ( 'overflowing' ) ;
}
2023-01-10 05:07:57 +01:00
}
_onPeers ( msg ) {
2023-09-13 18:15:01 +02:00
msg . peers . forEach ( peer => this . _joinPeer ( peer , msg . roomType , msg . roomId ) ) ;
2018-09-21 16:05:03 +02:00
}
2022-12-22 22:41:26 +01:00
_onPeerDisconnected ( peerId ) {
2018-10-09 15:45:07 +02:00
const $peer = $ ( peerId ) ;
if ( ! $peer ) return ;
$peer . remove ( ) ;
2023-11-24 16:25:30 +01:00
this . _evaluateOverflowingPeers ( ) ;
2018-09-21 16:05:03 +02:00
}
2023-09-13 18:15:01 +02:00
_onRoomTypeRemoved ( peerId , roomType ) {
const peer = this . peers [ peerId ] ;
2023-05-04 17:38:51 +02:00
2023-09-13 18:15:01 +02:00
if ( ! peer ) return ;
delete peer . _roomIds [ roomType ] ;
this . _redrawPeerRoomTypes ( peerId )
2023-01-10 05:07:57 +01:00
}
2023-01-17 10:41:50 +01:00
_onSetProgress ( progress ) {
const $peer = $ ( progress . peerId ) ;
2018-10-09 15:45:07 +02:00
if ( ! $peer ) return ;
2023-01-17 10:41:50 +01:00
$peer . ui . setProgress ( progress . progress , progress . status )
2018-09-21 16:05:03 +02:00
}
2023-01-22 16:14:27 +01:00
_onDrop ( e ) {
2024-02-01 00:07:41 +01:00
if ( this . shareMode . active || Dialog . anyDialogShown ( ) ) return ;
2023-01-22 16:14:27 +01:00
e . preventDefault ( ) ;
2023-11-24 16:25:30 +01:00
2024-02-01 00:07:41 +01:00
this . _onDragEnd ( ) ;
2023-11-24 16:25:30 +01:00
2025-02-13 12:52:33 +01:00
if ( $$ ( 'x-peer' ) && $$ ( 'x-peer' ) . contains ( e . target ) ) return ; // dropped on peer
2024-02-01 00:07:41 +01:00
2025-02-13 12:52:33 +01:00
let files = e . dataTransfer . files ;
let text = e . dataTransfer . getData ( "text" ) ;
// convert FileList to Array
files = [ ... files ] ;
2024-02-01 00:07:41 +01:00
if ( files . length > 0 ) {
Events . fire ( 'activate-share-mode' , {
files : files
} ) ;
}
else if ( text . length > 0 ) {
Events . fire ( 'activate-share-mode' , {
text : text
} ) ;
2023-01-22 16:14:27 +01:00
}
}
2023-01-22 17:33:19 +01:00
_onDragOver ( e ) {
2023-11-24 16:25:30 +01:00
if ( this . shareMode . active || Dialog . anyDialogShown ( ) ) return ;
2024-02-01 00:07:41 +01:00
e . preventDefault ( ) ;
2023-11-08 20:36:46 +01:00
this . $xInstructions . setAttribute ( 'drop-bg' , true ) ;
this . $xNoPeers . setAttribute ( 'drop-bg' , true ) ;
2023-01-22 16:14:27 +01:00
}
_onDragEnd ( ) {
2023-11-08 20:36:46 +01:00
this . $xInstructions . removeAttribute ( 'drop-bg' ) ;
2023-01-22 17:33:19 +01:00
this . $xNoPeers . removeAttribute ( 'drop-bg' ) ;
2023-01-22 16:14:27 +01:00
}
2020-12-22 21:23:10 +01:00
_onPaste ( e ) {
2023-11-24 16:25:30 +01:00
// prevent send on paste when dialog is open
if ( this . shareMode . active || Dialog . anyDialogShown ( ) ) return ;
e . preventDefault ( )
2025-02-13 12:52:33 +01:00
let files = e . clipboardData . files ;
let text = e . clipboardData . getData ( "Text" ) ;
// convert FileList to Array
files = [ ... files ] ;
2023-11-24 16:25:30 +01:00
if ( files . length > 0 ) {
Events . fire ( 'activate-share-mode' , { files : files } ) ;
} else if ( text . length > 0 ) {
if ( ShareTextDialog . isApproveShareTextSet ( ) ) {
Events . fire ( 'share-text-dialog' , text ) ;
} else {
Events . fire ( 'activate-share-mode' , { text : text } ) ;
}
2022-11-09 17:43:27 +01:00
}
}
2023-11-24 16:25:30 +01:00
async _activateShareMode ( files = [ ] , text = "" ) {
if ( this . shareMode . active || ( files . length === 0 && text . length === 0 ) ) return ;
2022-11-09 17:43:27 +01:00
2023-11-24 16:25:30 +01:00
this . _activateCallback = e => this . _sendShareData ( e ) ;
this . _editShareTextCallback = _ => {
this . _deactivateShareMode ( ) ;
Events . fire ( 'share-text-dialog' , text ) ;
} ;
2023-07-06 21:29:36 +02:00
2023-11-24 16:25:30 +01:00
Events . on ( 'share-mode-pointerdown' , this . _activateCallback ) ;
2023-12-05 18:51:56 +01:00
const sharedText = Localization . getTranslation ( "instructions.activate-share-mode-shared-text" ) ;
2023-11-24 16:25:30 +01:00
const andOtherFilesPlural = Localization . getTranslation ( "instructions.activate-share-mode-and-other-files-plural" , null , { count : files . length - 1 } ) ;
const andOtherFiles = Localization . getTranslation ( "instructions.activate-share-mode-and-other-file" ) ;
let descriptorComplete , descriptorItem , descriptorOther , descriptorInstructions ;
if ( files . length > 2 ) {
// files shared
descriptorItem = files [ 0 ] . name ;
descriptorOther = andOtherFilesPlural ;
descriptorComplete = ` ${ descriptorItem } ${ descriptorOther } ` ;
}
else if ( files . length === 2 ) {
descriptorItem = files [ 0 ] . name ;
descriptorOther = andOtherFiles ;
descriptorComplete = ` ${ descriptorItem } ${ descriptorOther } ` ;
} else if ( files . length === 1 ) {
descriptorItem = files [ 0 ] . name ;
descriptorComplete = descriptorItem ;
}
else {
// text shared
descriptorItem = text . replace ( /\s/g , " " ) ;
2023-12-05 18:51:56 +01:00
descriptorComplete = sharedText ;
2023-11-24 16:25:30 +01:00
}
if ( files . length > 0 ) {
if ( descriptorOther ) {
this . $shareModeDescriptorOther . innerText = descriptorOther ;
this . $shareModeDescriptorOther . removeAttribute ( 'hidden' ) ;
2023-11-02 02:56:01 +01:00
}
2023-11-24 16:25:30 +01:00
if ( files . length > 1 ) {
descriptorInstructions = Localization . getTranslation ( "instructions.activate-share-mode-shared-files-plural" , null , { count : files . length } ) ;
2023-11-02 02:56:01 +01:00
}
else {
2023-11-24 16:25:30 +01:00
descriptorInstructions = Localization . getTranslation ( "instructions.activate-share-mode-shared-file" ) ;
2022-11-09 17:43:27 +01:00
}
2023-11-24 16:25:30 +01:00
if ( files [ 0 ] . type . split ( '/' ) [ 0 ] === 'image' ) {
2023-12-11 17:19:56 +01:00
try {
2023-12-12 14:07:15 +01:00
let imageUrl = await getThumbnailAsDataUrl ( files [ 0 ] , 80 , null , 0.9 ) ;
2023-12-11 17:19:56 +01:00
this . $shareModeImageThumb . style . backgroundImage = ` url( ${ imageUrl } ) ` ;
this . $shareModeImageThumb . removeAttribute ( 'hidden' ) ;
} catch ( e ) {
console . error ( e ) ;
this . $shareModeFileThumb . removeAttribute ( 'hidden' ) ;
}
2023-11-24 16:25:30 +01:00
} else {
this . $shareModeFileThumb . removeAttribute ( 'hidden' ) ;
}
}
else {
this . $shareModeTextThumb . removeAttribute ( 'hidden' ) ;
2022-11-09 17:43:27 +01:00
2023-11-24 16:25:30 +01:00
this . $shareModeEditBtn . addEventListener ( 'click' , this . _editShareTextCallback ) ;
this . $shareModeEditBtn . removeAttribute ( 'hidden' ) ;
2022-11-09 17:43:27 +01:00
2023-11-24 16:25:30 +01:00
descriptorInstructions = Localization . getTranslation ( "instructions.activate-share-mode-shared-text" ) ;
}
2023-11-08 20:31:57 +01:00
2023-11-24 16:25:30 +01:00
const desktop = Localization . getTranslation ( "instructions.x-instructions-share-mode_desktop" , null , { descriptor : descriptorInstructions } ) ;
const mobile = Localization . getTranslation ( "instructions.x-instructions-share-mode_mobile" , null , { descriptor : descriptorInstructions } ) ;
2022-11-09 17:43:27 +01:00
2023-11-24 16:25:30 +01:00
this . $xInstructions . setAttribute ( 'desktop' , desktop ) ;
this . $xInstructions . setAttribute ( 'mobile' , mobile ) ;
2022-11-09 17:43:27 +01:00
2023-11-24 16:25:30 +01:00
this . $sharePanel . removeAttribute ( 'hidden' ) ;
2022-11-09 17:43:27 +01:00
2023-11-24 16:25:30 +01:00
this . $shareModeDescriptor . removeAttribute ( 'hidden' ) ;
this . $shareModeDescriptorItem . innerText = descriptorItem ;
this . shareMode . active = true ;
2023-12-05 18:51:56 +01:00
this . shareMode . descriptor = descriptorComplete ;
2023-11-24 16:25:30 +01:00
this . shareMode . files = files ;
this . shareMode . text = text ;
console . log ( 'Share mode activated.' ) ;
Events . fire ( 'share-mode-changed' , {
active : true ,
descriptor : descriptorComplete
} ) ;
2022-11-09 17:43:27 +01:00
}
2023-11-24 16:25:30 +01:00
async _reloadShareMode ( ) {
// If shareMode is active only
if ( ! this . shareMode . active ) return ;
let files = this . shareMode . files ;
let text = this . shareMode . text ;
await this . _deactivateShareMode ( ) ;
await this . _activateShareMode ( files , text ) ;
2022-11-09 17:43:27 +01:00
}
2023-11-24 16:25:30 +01:00
async _deactivateShareMode ( ) {
if ( ! this . shareMode . active ) return ;
2022-11-09 17:43:27 +01:00
2023-11-24 16:25:30 +01:00
this . shareMode . active = false ;
2023-12-05 18:51:56 +01:00
this . shareMode . descriptor = "" ;
2023-11-24 16:25:30 +01:00
this . shareMode . files = [ ] ;
this . shareMode . text = "" ;
2023-01-18 15:28:57 +01:00
2023-11-24 16:25:30 +01:00
Events . off ( 'share-mode-pointerdown' , this . _activateCallback ) ;
2023-01-18 15:28:57 +01:00
2023-11-24 16:25:30 +01:00
const desktop = Localization . getTranslation ( "instructions.x-instructions_desktop" ) ;
const mobile = Localization . getTranslation ( "instructions.x-instructions_mobile" ) ;
2023-01-18 15:28:57 +01:00
2023-11-24 16:25:30 +01:00
this . $xInstructions . setAttribute ( 'desktop' , desktop ) ;
this . $xInstructions . setAttribute ( 'mobile' , mobile ) ;
2022-11-09 17:43:27 +01:00
2023-11-24 16:25:30 +01:00
this . $sharePanel . setAttribute ( 'hidden' , true ) ;
this . $shareModeImageThumb . setAttribute ( 'hidden' , true ) ;
this . $shareModeFileThumb . setAttribute ( 'hidden' , true ) ;
this . $shareModeTextThumb . setAttribute ( 'hidden' , true ) ;
this . $shareModeDescriptorItem . innerHTML = "" ;
this . $shareModeDescriptorItem . classList . remove ( 'cursive' ) ;
this . $shareModeDescriptorOther . innerHTML = "" ;
this . $shareModeDescriptorOther . setAttribute ( 'hidden' , true ) ;
this . $shareModeEditBtn . removeEventListener ( 'click' , this . _editShareTextCallback ) ;
this . $shareModeEditBtn . setAttribute ( 'hidden' , true ) ;
console . log ( 'Share mode deactivated.' )
Events . fire ( 'share-mode-changed' , { active : false } ) ;
2022-11-09 17:43:27 +01:00
}
2023-11-24 16:25:30 +01:00
_sendShareData ( e ) {
// send the shared file/text content
2022-11-09 17:43:27 +01:00
const peerId = e . detail . peerId ;
2023-11-24 16:25:30 +01:00
const files = this . shareMode . files ;
const text = this . shareMode . text ;
2022-11-09 17:43:27 +01:00
if ( files . length > 0 ) {
2020-03-03 09:15:32 -05:00
Events . fire ( 'files-selected' , {
files : files ,
2022-11-09 17:43:27 +01:00
to : peerId
} ) ;
2023-11-02 02:56:01 +01:00
}
else if ( text . length > 0 ) {
2022-11-09 17:43:27 +01:00
Events . fire ( 'send-text' , {
text : text ,
to : peerId
2020-03-03 09:15:32 -05:00
} ) ;
}
2018-09-21 16:05:03 +02:00
}
}
class PeerUI {
2023-11-24 16:25:30 +01:00
constructor ( peer , connectionHash , shareMode ) {
2023-11-08 20:31:57 +01:00
this . $xInstructions = $$ ( 'x-instructions' ) ;
this . $xPeers = $$ ( 'x-peers' ) ;
2023-03-01 10:04:37 +01:00
this . _peer = peer ;
2023-03-06 15:39:24 +01:00
this . _connectionHash =
` ${ connectionHash . substring ( 0 , 4 ) } ${ connectionHash . substring ( 4 , 8 ) } ${ connectionHash . substring ( 8 , 12 ) } ${ connectionHash . substring ( 12 , 16 ) } ` ;
2023-11-08 20:31:57 +01:00
2023-11-24 16:25:30 +01:00
// This is needed if the ShareMode is started BEFORE the PeerUI is drawn.
2025-02-13 10:24:27 +01:00
this . _shareMode = shareMode ;
2023-11-08 20:31:57 +01:00
2023-03-01 10:04:37 +01:00
this . _initDom ( ) ;
2023-11-08 20:31:57 +01:00
this . $xPeers . appendChild ( this . $el ) ;
2023-03-01 10:04:37 +01:00
Events . fire ( 'peer-added' ) ;
2023-11-24 16:25:30 +01:00
// ShareMode
Events . on ( 'share-mode-changed' , e => this . _onShareModeChanged ( e . detail . active , e . detail . descriptor ) ) ;
2023-03-01 10:04:37 +01:00
}
2018-09-21 16:05:03 +02:00
html ( ) {
2025-02-13 10:24:27 +01:00
let title = this . _shareMode . active
? Localization . getTranslation ( "peer-ui.click-to-send-share-mode" , null , { descriptor : this . _shareMode . descriptor } )
2023-12-05 18:51:56 +01:00
: Localization . getTranslation ( "peer-ui.click-to-send" ) ;
2023-11-24 16:25:30 +01:00
2023-01-18 15:28:57 +01:00
this . $el . innerHTML = `
2023-09-13 18:15:01 +02:00
< label class = "column center pointer" title = "${title}" >
2023-11-24 16:25:30 +01:00
< input type = "file" multiple / >
2023-03-01 10:04:37 +01:00
< x - icon >
< div class = "icon-wrapper" shadow = "1" >
< svg class = "icon" > < use xlink : href = "#" / > < / s v g >
< / d i v >
< div class = "highlight-wrapper center" >
2023-09-13 18:15:01 +02:00
< div class = "highlight highlight-room-ip" shadow = "1" > < / d i v >
< div class = "highlight highlight-room-secret" shadow = "1" > < / d i v >
< div class = "highlight highlight-room-public-id" shadow = "1" > < / d i v >
2023-03-01 10:04:37 +01:00
< / d i v >
2018-09-21 16:05:03 +02:00
< / x - i c o n >
< div class = "progress" >
< div class = "circle" > < / d i v >
< div class = "circle right" > < / d i v >
< / d i v >
2023-03-01 10:04:37 +01:00
< div class = "device-descriptor" >
< div class = "name font-subheading" > < / d i v >
< div class = "device-name font-body2" > < / d i v >
< div class = "status font-body2" > < / d i v >
< / d i v >
2022-11-09 17:43:27 +01:00
< / l a b e l > ` ;
2023-01-18 15:28:57 +01:00
this . $el . querySelector ( 'svg use' ) . setAttribute ( 'xlink:href' , this . _icon ( ) ) ;
this . $el . querySelector ( '.name' ) . textContent = this . _displayName ( ) ;
this . $el . querySelector ( '.device-name' ) . textContent = this . _deviceName ( ) ;
2023-11-24 16:25:30 +01:00
this . $label = this . $el . querySelector ( 'label' ) ;
this . $input = this . $el . querySelector ( 'input' ) ;
2018-09-21 16:05:03 +02:00
}
2023-09-13 18:15:01 +02:00
addTypesToClassList ( ) {
if ( this . _peer . _isSameBrowser ( ) ) {
this . $el . classList . add ( ` type-same-browser ` ) ;
}
Object . keys ( this . _peer . _roomIds ) . forEach ( roomType => this . $el . classList . add ( ` type- ${ roomType } ` ) ) ;
2023-11-08 20:31:57 +01:00
if ( ! this . _peer . rtcSupported || ! window . isRtcSupported ) this . $el . classList . add ( 'ws-peer' ) ;
2023-09-13 18:15:01 +02:00
}
2018-09-21 16:05:03 +02:00
_initDom ( ) {
2023-01-18 15:28:57 +01:00
this . $el = document . createElement ( 'x-peer' ) ;
this . $el . id = this . _peer . id ;
this . $el . ui = this ;
2023-03-01 10:04:37 +01:00
this . $el . classList . add ( 'center' ) ;
2023-09-13 18:15:01 +02:00
this . addTypesToClassList ( ) ;
2023-01-18 15:28:57 +01:00
this . html ( ) ;
2023-11-24 16:25:30 +01:00
this . _createCallbacks ( ) ;
this . _evaluateShareMode ( ) ;
this . _bindListeners ( ) ;
}
_onShareModeChanged ( active = false , descriptor = "" ) {
// This is needed if the ShareMode is started AFTER the PeerUI is drawn.
2025-02-13 10:24:27 +01:00
this . _shareMode . active = active ;
this . _shareMode . descriptor = descriptor ;
2023-11-24 16:25:30 +01:00
this . _evaluateShareMode ( ) ;
2023-01-18 15:28:57 +01:00
this . _bindListeners ( ) ;
}
2023-11-24 16:25:30 +01:00
_evaluateShareMode ( ) {
let title ;
2025-02-13 10:24:27 +01:00
if ( ! this . _shareMode . active ) {
2023-11-24 16:25:30 +01:00
title = Localization . getTranslation ( "peer-ui.click-to-send" ) ;
this . $input . removeAttribute ( 'disabled' ) ;
}
else {
2025-02-13 10:24:27 +01:00
title = Localization . getTranslation ( "peer-ui.click-to-send-share-mode" , null , { descriptor : this . _shareMode . descriptor } ) ;
2023-11-24 16:25:30 +01:00
this . $input . setAttribute ( 'disabled' , true ) ;
}
this . $label . setAttribute ( 'title' , title ) ;
}
_createCallbacks ( ) {
this . _callbackInput = e => this . _onFilesSelected ( e ) ;
this . _callbackClickSleep = _ => NoSleepUI . enable ( ) ;
this . _callbackTouchStartSleep = _ => NoSleepUI . enable ( ) ;
this . _callbackDrop = e => this . _onDrop ( e ) ;
this . _callbackDragEnd = e => this . _onDragEnd ( e ) ;
this . _callbackDragLeave = e => this . _onDragEnd ( e ) ;
this . _callbackDragOver = e => this . _onDragOver ( e ) ;
this . _callbackContextMenu = e => this . _onRightClick ( e ) ;
this . _callbackTouchStart = e => this . _onTouchStart ( e ) ;
this . _callbackTouchEnd = e => this . _onTouchEnd ( e ) ;
this . _callbackPointerDown = e => this . _onPointerDown ( e ) ;
}
2023-01-18 15:28:57 +01:00
_bindListeners ( ) {
2025-02-13 10:24:27 +01:00
if ( ! this . _shareMode . active ) {
2023-11-24 16:25:30 +01:00
// Remove Events Share mode
2023-01-18 15:28:57 +01:00
this . $el . removeEventListener ( 'pointerdown' , this . _callbackPointerDown ) ;
// Add Events Normal Mode
this . $el . querySelector ( 'input' ) . addEventListener ( 'change' , this . _callbackInput ) ;
this . $el . addEventListener ( 'click' , this . _callbackClickSleep ) ;
this . $el . addEventListener ( 'touchstart' , this . _callbackTouchStartSleep ) ;
this . $el . addEventListener ( 'drop' , this . _callbackDrop ) ;
this . $el . addEventListener ( 'dragend' , this . _callbackDragEnd ) ;
this . $el . addEventListener ( 'dragleave' , this . _callbackDragLeave ) ;
this . $el . addEventListener ( 'dragover' , this . _callbackDragOver ) ;
this . $el . addEventListener ( 'contextmenu' , this . _callbackContextMenu ) ;
this . $el . addEventListener ( 'touchstart' , this . _callbackTouchStart ) ;
this . $el . addEventListener ( 'touchend' , this . _callbackTouchEnd ) ;
2023-11-02 02:56:01 +01:00
}
else {
2023-01-18 15:28:57 +01:00
// Remove Events Normal Mode
this . $el . removeEventListener ( 'click' , this . _callbackClickSleep ) ;
this . $el . removeEventListener ( 'touchstart' , this . _callbackTouchStartSleep ) ;
this . $el . removeEventListener ( 'drop' , this . _callbackDrop ) ;
this . $el . removeEventListener ( 'dragend' , this . _callbackDragEnd ) ;
this . $el . removeEventListener ( 'dragleave' , this . _callbackDragLeave ) ;
this . $el . removeEventListener ( 'dragover' , this . _callbackDragOver ) ;
this . $el . removeEventListener ( 'contextmenu' , this . _callbackContextMenu ) ;
this . $el . removeEventListener ( 'touchstart' , this . _callbackTouchStart ) ;
this . $el . removeEventListener ( 'touchend' , this . _callbackTouchEnd ) ;
2023-11-24 16:25:30 +01:00
// Add Events Share mode
2023-01-18 15:28:57 +01:00
this . $el . addEventListener ( 'pointerdown' , this . _callbackPointerDown ) ;
2022-11-09 17:43:27 +01:00
}
}
_onPointerDown ( e ) {
// Prevents triggering of event twice on touch devices
e . stopPropagation ( ) ;
e . preventDefault ( ) ;
2023-11-24 16:25:30 +01:00
Events . fire ( 'share-mode-pointerdown' , {
2022-11-09 17:43:27 +01:00
peerId : this . _peer . id
} ) ;
2018-09-21 16:05:03 +02:00
}
2020-12-19 21:05:48 +01:00
_displayName ( ) {
2019-08-28 20:16:04 +05:30
return this . _peer . name . displayName ;
2018-09-21 16:05:03 +02:00
}
2020-12-19 21:05:48 +01:00
_deviceName ( ) {
return this . _peer . name . deviceName ;
}
2023-09-13 18:15:01 +02:00
_badgeClassName ( ) {
const roomTypes = Object . keys ( this . _peer . _roomIds ) ;
return roomTypes . includes ( 'secret' )
? 'badge-room-secret'
: roomTypes . includes ( 'ip' )
? 'badge-room-ip'
: 'badge-room-public-id' ;
}
2018-09-21 16:05:03 +02:00
_icon ( ) {
const device = this . _peer . name . device || this . _peer . name ;
if ( device . type === 'mobile' ) {
return '#phone-iphone' ;
}
if ( device . type === 'tablet' ) {
return '#tablet-mac' ;
}
return '#desktop-mac' ;
}
_onFilesSelected ( e ) {
const $input = e . target ;
const files = $input . files ;
2024-02-06 13:42:47 +01:00
if ( files . length === 0 ) return ;
2018-09-21 16:05:03 +02:00
Events . fire ( 'files-selected' , {
files : files ,
to : this . _peer . id
} ) ;
2023-01-17 10:41:50 +01:00
$input . files = null ; // reset input
2018-09-21 16:05:03 +02:00
}
2023-01-17 10:41:50 +01:00
setProgress ( progress , status ) {
2023-01-18 15:28:57 +01:00
const $progress = this . $el . querySelector ( '.progress' ) ;
2023-01-17 10:41:50 +01:00
if ( 0.5 < progress && progress < 1 ) {
2023-01-18 15:28:57 +01:00
$progress . classList . add ( 'over50' ) ;
2023-11-02 02:56:01 +01:00
}
else {
2023-01-18 15:28:57 +01:00
$progress . classList . remove ( 'over50' ) ;
2018-09-21 16:05:03 +02:00
}
2023-01-17 10:41:50 +01:00
if ( progress < 1 ) {
2023-07-06 21:29:36 +02:00
if ( status !== this . currentStatus ) {
let statusName = {
"prepare" : Localization . getTranslation ( "peer-ui.preparing" ) ,
"transfer" : Localization . getTranslation ( "peer-ui.transferring" ) ,
"process" : Localization . getTranslation ( "peer-ui.processing" ) ,
"wait" : Localization . getTranslation ( "peer-ui.waiting" )
} [ status ] ;
this . $el . setAttribute ( 'status' , status ) ;
this . $el . querySelector ( '.status' ) . innerText = statusName ;
this . currentStatus = status ;
}
2023-11-02 02:56:01 +01:00
}
else {
2023-01-17 10:41:50 +01:00
this . $el . removeAttribute ( 'status' ) ;
2023-07-06 21:29:36 +02:00
this . $el . querySelector ( '.status' ) . innerHTML = '' ;
2023-01-17 10:41:50 +01:00
progress = 0 ;
2023-07-06 21:29:36 +02:00
this . currentStatus = null ;
2023-01-17 10:41:50 +01:00
}
2018-09-21 16:05:03 +02:00
const degrees = ` rotate( ${ 360 * progress } deg) ` ;
2023-01-18 15:28:57 +01:00
$progress . style . setProperty ( '--progress' , degrees ) ;
2018-09-21 16:05:03 +02:00
}
_onDrop ( e ) {
2025-02-13 10:24:27 +01:00
if ( this . _shareMode . active || Dialog . anyDialogShown ( ) ) return ;
2024-02-01 00:07:41 +01:00
2018-09-21 16:05:03 +02:00
e . preventDefault ( ) ;
2023-11-24 16:25:30 +01:00
2024-02-01 00:07:41 +01:00
this . _onDragEnd ( ) ;
const peerId = this . _peer . id ;
const files = e . dataTransfer . files ;
const text = e . dataTransfer . getData ( "text" ) ;
2023-11-24 16:25:30 +01:00
2024-02-01 00:07:41 +01:00
if ( files . length > 0 ) {
2023-11-24 16:25:30 +01:00
Events . fire ( 'files-selected' , {
2024-02-01 00:07:41 +01:00
files : files ,
to : peerId
} ) ;
}
else if ( text . length > 0 ) {
Events . fire ( 'send-text' , {
text : text ,
to : peerId
2023-11-24 16:25:30 +01:00
} ) ;
}
2018-09-21 16:05:03 +02:00
}
_onDragOver ( ) {
2023-11-08 20:36:46 +01:00
this . $el . setAttribute ( 'drop' , true ) ;
this . $xInstructions . setAttribute ( 'drop-peer' , true ) ;
2018-09-21 16:05:03 +02:00
}
_onDragEnd ( ) {
this . $el . removeAttribute ( 'drop' ) ;
2023-11-08 20:36:46 +01:00
this . $xInstructions . removeAttribute ( 'drop-peer' ) ;
2018-09-21 16:05:03 +02:00
}
_onRightClick ( e ) {
e . preventDefault ( ) ;
2023-03-01 10:04:37 +01:00
Events . fire ( 'text-recipient' , {
peerId : this . _peer . id ,
deviceName : e . target . closest ( 'x-peer' ) . querySelector ( '.name' ) . innerText
} ) ;
2018-09-21 16:05:03 +02:00
}
2023-03-01 10:04:37 +01:00
_onTouchStart ( e ) {
2018-09-21 16:05:03 +02:00
this . _touchStart = Date . now ( ) ;
2023-11-02 01:22:41 +01:00
this . _touchTimer = setTimeout ( ( ) => this . _onTouchEnd ( e ) , 610 ) ;
2018-09-21 16:05:03 +02:00
}
_onTouchEnd ( e ) {
if ( Date . now ( ) - this . _touchStart < 500 ) {
clearTimeout ( this . _touchTimer ) ;
2023-11-02 02:56:01 +01:00
}
else if ( this . _touchTimer ) { // this was a long tap
2023-03-01 10:04:37 +01:00
e . preventDefault ( ) ;
Events . fire ( 'text-recipient' , {
peerId : this . _peer . id ,
deviceName : e . target . closest ( 'x-peer' ) . querySelector ( '.name' ) . innerText
} ) ;
2018-09-21 16:05:03 +02:00
}
2023-03-01 10:04:37 +01:00
this . _touchTimer = null ;
2018-09-21 16:05:03 +02:00
}
}
class Dialog {
2023-02-08 12:55:28 +01:00
constructor ( id ) {
2018-09-21 16:05:03 +02:00
this . $el = $ ( id ) ;
2023-11-29 17:42:19 +01:00
this . $autoFocus = this . $el . querySelector ( '[autofocus]' ) ;
this . $xBackground = this . $el . querySelector ( 'x-background' ) ;
this . $closeBtns = this . $el . querySelectorAll ( '[close]' ) ;
this . $closeBtns . forEach ( el => {
2023-11-02 02:56:01 +01:00
el . addEventListener ( 'click' , _ => this . hide ( ) )
} ) ;
2023-10-11 17:56:40 +02:00
Events . on ( 'peer-disconnected' , e => this . _onPeerDisconnected ( e . detail ) ) ;
2018-09-21 16:05:03 +02:00
}
2023-11-24 16:25:30 +01:00
static anyDialogShown ( ) {
return document . querySelectorAll ( 'x-dialog[show]' ) . length > 0 ;
}
2018-09-21 16:05:03 +02:00
show ( ) {
2023-11-29 17:42:19 +01:00
if ( this . $xBackground ) {
this . $xBackground . scrollTop = 0 ;
}
2023-11-08 20:36:46 +01:00
this . $el . setAttribute ( 'show' , true ) ;
2023-11-29 17:42:19 +01:00
if ( ! window . isMobile && this . $autoFocus ) {
this . $autoFocus . focus ( ) ;
}
2018-09-21 16:05:03 +02:00
}
2023-05-09 03:21:10 +02:00
isShown ( ) {
return ! ! this . $el . attributes [ "show" ] ;
}
2018-09-21 16:05:03 +02:00
hide ( ) {
this . $el . removeAttribute ( 'show' ) ;
2023-12-12 14:19:41 +01:00
if ( ! window . isMobile ) {
2023-01-10 05:07:57 +01:00
document . activeElement . blur ( ) ;
window . blur ( ) ;
}
2023-11-20 03:53:43 +01:00
document . title = 'PairDrop | Transfer Files Cross-Platform. No Setup, No Signup.' ;
2023-10-11 23:22:10 +02:00
changeFavicon ( "images/favicon-96x96.png" ) ;
2023-05-09 03:21:10 +02:00
this . correspondingPeerId = undefined ;
2023-01-23 04:51:22 +01:00
}
_onPeerDisconnected ( peerId ) {
2023-05-09 03:21:10 +02:00
if ( this . isShown ( ) && this . correspondingPeerId === peerId ) {
2023-01-23 04:51:22 +01:00
this . hide ( ) ;
2023-07-06 21:29:36 +02:00
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.selected-peer-left" ) ) ;
2023-01-23 04:51:22 +01:00
}
2018-09-21 16:05:03 +02:00
}
2023-11-24 16:25:30 +01:00
_evaluateOverflowing ( element ) {
if ( element . clientHeight < element . scrollHeight ) {
element . classList . add ( 'overflowing' ) ;
}
else {
element . classList . remove ( 'overflowing' ) ;
}
}
2018-09-21 16:05:03 +02:00
}
2023-08-30 14:57:40 +02:00
class LanguageSelectDialog extends Dialog {
constructor ( ) {
super ( 'language-select-dialog' ) ;
this . $languageSelectBtn = $ ( 'language-selector' ) ;
this . $languageSelectBtn . addEventListener ( 'click' , _ => this . show ( ) ) ;
2023-12-11 21:44:13 +01:00
this . $languageButtons = this . $el . querySelectorAll ( ".language-buttons .btn" ) ;
2023-08-30 14:57:40 +02:00
this . $languageButtons . forEach ( $btn => {
$btn . addEventListener ( "click" , e => this . selectLanguage ( e ) ) ;
} )
Events . on ( 'keydown' , e => this . _onKeyDown ( e ) ) ;
}
_onKeyDown ( e ) {
2023-11-24 16:25:30 +01:00
if ( ! this . isShown ( ) ) return ;
if ( e . code === "Escape" ) {
2023-08-30 14:57:40 +02:00
this . hide ( ) ;
}
}
show ( ) {
2023-12-11 21:44:13 +01:00
let locale = Localization . getLocale ( ) ;
this . currentLanguageBtn = Localization . isSystemLocale ( )
? this . $languageButtons [ 0 ]
: this . $el . querySelector ( ` .btn[value=" ${ locale } "] ` ) ;
this . currentLanguageBtn . classList . add ( "current" ) ;
2023-08-30 14:57:40 +02:00
super . show ( ) ;
}
2023-12-11 21:44:13 +01:00
hide ( ) {
this . currentLanguageBtn . classList . remove ( "current" ) ;
super . hide ( ) ;
}
2023-08-30 14:57:40 +02:00
selectLanguage ( e ) {
e . preventDefault ( )
let languageCode = e . target . value ;
if ( languageCode ) {
2023-11-24 17:32:40 +01:00
localStorage . setItem ( 'language_code' , languageCode ) ;
2023-11-02 02:56:01 +01:00
}
else {
2023-11-24 17:32:40 +01:00
localStorage . removeItem ( 'language_code' ) ;
2023-08-30 14:57:40 +02:00
}
Localization . setTranslation ( languageCode )
. then ( _ => this . hide ( ) ) ;
}
}
2018-09-21 16:05:03 +02:00
class ReceiveDialog extends Dialog {
2023-02-08 12:55:28 +01:00
constructor ( id ) {
super ( id ) ;
2023-03-03 12:01:43 +01:00
this . $fileDescription = this . $el . querySelector ( '.file-description' ) ;
this . $displayName = this . $el . querySelector ( '.display-name' ) ;
this . $fileStem = this . $el . querySelector ( '.file-stem' ) ;
this . $fileExtension = this . $el . querySelector ( '.file-extension' ) ;
this . $fileOther = this . $el . querySelector ( '.file-other' ) ;
this . $fileSize = this . $el . querySelector ( '.file-size' ) ;
this . $previewBox = this . $el . querySelector ( '.file-preview' ) ;
this . $receiveTitle = this . $el . querySelector ( 'h2:first-of-type' ) ;
2023-01-17 10:41:50 +01:00
}
_formatFileSize ( bytes ) {
2023-01-23 20:09:35 +01:00
// 1 GB = 1024 MB = 1024^2 KB = 1024^3 B
// 1024^2 = 104876; 1024^3 = 1073741824
if ( bytes >= 1073741824 ) {
return Math . round ( 10 * bytes / 1073741824 ) / 10 + ' GB' ;
2023-11-02 02:56:01 +01:00
}
else if ( bytes >= 1048576 ) {
2023-01-23 20:09:35 +01:00
return Math . round ( bytes / 1048576 ) + ' MB' ;
2023-11-02 02:56:01 +01:00
}
else if ( bytes > 1024 ) {
2023-01-25 09:59:38 +01:00
return Math . round ( bytes / 1024 ) + ' KB' ;
2023-11-02 02:56:01 +01:00
}
else {
2023-01-17 10:41:50 +01:00
return bytes + ' Bytes' ;
}
}
2023-03-03 12:01:43 +01:00
2023-09-13 18:15:01 +02:00
_parseFileData ( displayName , connectionHash , files , imagesOnly , totalSize , badgeClassName ) {
2023-09-13 17:44:49 +02:00
let fileOther = "" ;
if ( files . length === 2 ) {
fileOther = imagesOnly
? Localization . getTranslation ( "dialogs.file-other-description-image" )
: Localization . getTranslation ( "dialogs.file-other-description-file" ) ;
2023-11-02 02:56:01 +01:00
}
else if ( files . length >= 2 ) {
2023-09-13 17:44:49 +02:00
fileOther = imagesOnly
? Localization . getTranslation ( "dialogs.file-other-description-image-plural" , null , { count : files . length - 1 } )
: Localization . getTranslation ( "dialogs.file-other-description-file-plural" , null , { count : files . length - 1 } ) ;
2023-03-03 12:01:43 +01:00
}
2023-09-13 17:44:49 +02:00
this . $fileOther . innerText = fileOther ;
2023-03-03 12:01:43 +01:00
const fileName = files [ 0 ] . name ;
const fileNameSplit = fileName . split ( '.' ) ;
const fileExtension = '.' + fileNameSplit [ fileNameSplit . length - 1 ] ;
this . $fileStem . innerText = fileName . substring ( 0 , fileName . length - fileExtension . length ) ;
this . $fileExtension . innerText = fileExtension ;
2023-09-13 18:15:01 +02:00
this . $fileSize . innerText = this . _formatFileSize ( totalSize ) ;
2023-03-03 12:01:43 +01:00
this . $displayName . innerText = displayName ;
2023-03-06 15:39:24 +01:00
this . $displayName . title = connectionHash ;
2023-09-13 18:15:01 +02:00
this . $displayName . classList . remove ( "badge-room-ip" , "badge-room-secret" , "badge-room-public-id" ) ;
this . $displayName . classList . add ( badgeClassName )
2023-03-03 12:01:43 +01:00
}
2023-01-17 10:41:50 +01:00
}
2023-01-17 14:19:51 +01:00
2023-01-17 10:41:50 +01:00
class ReceiveFileDialog extends ReceiveDialog {
2018-09-21 16:05:03 +02:00
constructor ( ) {
2023-03-01 10:44:57 +01:00
super ( 'receive-file-dialog' ) ;
2023-01-17 10:41:50 +01:00
2023-03-03 12:01:43 +01:00
this . $downloadBtn = this . $el . querySelector ( '#download-btn' ) ;
this . $shareBtn = this . $el . querySelector ( '#share-btn' ) ;
2023-01-17 10:41:50 +01:00
2023-09-13 18:15:01 +02:00
Events . on ( 'files-received' , e => this . _onFilesReceived ( e . detail . peerId , e . detail . files , e . detail . imagesOnly , e . detail . totalSize ) ) ;
2018-09-21 16:05:03 +02:00
this . _filesQueue = [ ] ;
}
2023-11-24 16:25:30 +01:00
async _onFilesReceived ( peerId , files , imagesOnly , totalSize ) {
2023-09-13 18:15:01 +02:00
const displayName = $ ( peerId ) . ui . _displayName ( ) ;
const connectionHash = $ ( peerId ) . ui . _connectionHash ;
const badgeClassName = $ ( peerId ) . ui . _badgeClassName ( ) ;
this . _filesQueue . push ( {
peerId : peerId ,
displayName : displayName ,
connectionHash : connectionHash ,
files : files ,
imagesOnly : imagesOnly ,
totalSize : totalSize ,
badgeClassName : badgeClassName
} ) ;
2023-01-17 10:41:50 +01:00
window . blop . play ( ) ;
2023-11-24 16:25:30 +01:00
await this . _nextFiles ( ) ;
2023-01-17 10:41:50 +01:00
}
2023-11-24 16:25:30 +01:00
async _nextFiles ( ) {
if ( this . _busy || ! this . _filesQueue . length ) return ;
2018-09-21 16:05:03 +02:00
this . _busy = true ;
2023-09-13 18:15:01 +02:00
const { peerId , displayName , connectionHash , files , imagesOnly , totalSize , badgeClassName } = this . _filesQueue . shift ( ) ;
2023-11-24 16:25:30 +01:00
await this . _displayFiles ( peerId , displayName , connectionHash , files , imagesOnly , totalSize , badgeClassName ) ;
2018-09-21 16:05:03 +02:00
}
2023-01-17 10:41:50 +01:00
createPreviewElement ( file ) {
2023-01-21 18:21:58 +01:00
return new Promise ( ( resolve , reject ) => {
2023-03-13 00:04:48 +01:00
try {
let mime = file . type . split ( '/' ) [ 0 ]
let previewElement = {
image : 'img' ,
audio : 'audio' ,
video : 'video'
}
2023-01-17 10:41:50 +01:00
2023-03-13 00:04:48 +01:00
if ( Object . keys ( previewElement ) . indexOf ( mime ) === - 1 ) {
resolve ( false ) ;
2023-11-02 02:56:01 +01:00
}
else {
2023-03-13 00:04:48 +01:00
let element = document . createElement ( previewElement [ mime ] ) ;
element . controls = true ;
element . onload = _ => {
this . $previewBox . appendChild ( element ) ;
resolve ( true ) ;
} ;
element . onloadeddata = _ => {
this . $previewBox . appendChild ( element ) ;
resolve ( true ) ;
} ;
element . onerror = _ => {
reject ( ` ${ mime } preview could not be loaded from type ${ file . type } ` ) ;
} ;
element . src = URL . createObjectURL ( file ) ;
}
} catch ( e ) {
reject ( ` preview could not be loaded from type ${ file . type } ` ) ;
2023-01-17 10:41:50 +01:00
}
} ) ;
}
2023-09-13 18:15:01 +02:00
async _displayFiles ( peerId , displayName , connectionHash , files , imagesOnly , totalSize , badgeClassName ) {
this . _parseFileData ( displayName , connectionHash , files , imagesOnly , totalSize , badgeClassName ) ;
2023-01-27 01:27:22 +01:00
2023-03-03 12:01:43 +01:00
let descriptor , url , filenameDownload ;
2023-01-17 10:41:50 +01:00
if ( files . length === 1 ) {
2023-07-06 21:29:36 +02:00
descriptor = imagesOnly
? Localization . getTranslation ( "dialogs.title-image" )
: Localization . getTranslation ( "dialogs.title-file" ) ;
2023-11-02 02:56:01 +01:00
}
else {
2023-07-06 21:29:36 +02:00
descriptor = imagesOnly
? Localization . getTranslation ( "dialogs.title-image-plural" )
: Localization . getTranslation ( "dialogs.title-file-plural" ) ;
2023-03-03 12:01:43 +01:00
}
2023-07-06 21:29:36 +02:00
this . $receiveTitle . innerText = Localization . getTranslation ( "dialogs.receive-title" , null , { descriptor : descriptor } ) ;
2023-01-17 10:41:50 +01:00
2023-03-03 12:01:43 +01:00
const canShare = ( window . iOS || window . android ) && ! ! navigator . share && navigator . canShare ( { files } ) ;
if ( canShare ) {
this . $shareBtn . removeAttribute ( 'hidden' ) ;
this . $shareBtn . onclick = _ => {
navigator . share ( { files : files } )
. catch ( err => {
console . error ( err ) ;
} ) ;
}
}
2023-01-17 10:41:50 +01:00
2023-03-03 12:01:43 +01:00
let downloadZipped = false ;
if ( files . length > 1 ) {
downloadZipped = true ;
try {
2023-01-27 01:27:22 +01:00
let bytesCompleted = 0 ;
zipper . createNewZipWriter ( ) ;
for ( let i = 0 ; i < files . length ; i ++ ) {
await zipper . addFile ( files [ i ] , {
onprogress : ( progress ) => {
Events . fire ( 'set-progress' , {
peerId : peerId ,
2023-03-03 12:01:43 +01:00
progress : ( bytesCompleted + progress ) / totalSize ,
2023-01-27 01:27:22 +01:00
status : 'process'
} )
}
} ) ;
bytesCompleted += files [ i ] . size ;
}
url = await zipper . getBlobURL ( ) ;
let now = new Date ( Date . now ( ) ) ;
let year = now . getFullYear ( ) . toString ( ) ;
let month = ( now . getMonth ( ) + 1 ) . toString ( ) ;
month = month . length < 2 ? "0" + month : month ;
let date = now . getDate ( ) . toString ( ) ;
date = date . length < 2 ? "0" + date : date ;
let hours = now . getHours ( ) . toString ( ) ;
hours = hours . length < 2 ? "0" + hours : hours ;
let minutes = now . getMinutes ( ) . toString ( ) ;
minutes = minutes . length < 2 ? "0" + minutes : minutes ;
filenameDownload = ` PairDrop_files_ ${ year + month + date } _ ${ hours + minutes } .zip ` ;
2023-03-03 12:01:43 +01:00
} catch ( e ) {
console . error ( e ) ;
downloadZipped = false ;
2023-01-17 10:41:50 +01:00
}
}
2023-11-24 16:25:30 +01:00
this . $downloadBtn . removeAttribute ( 'disabled' ) ;
2023-07-06 21:29:36 +02:00
this . $downloadBtn . innerText = Localization . getTranslation ( "dialogs.download" ) ;
2023-03-03 12:01:43 +01:00
this . $downloadBtn . onclick = _ => {
if ( downloadZipped ) {
let tmpZipBtn = document . createElement ( "a" ) ;
tmpZipBtn . download = filenameDownload ;
tmpZipBtn . href = url ;
tmpZipBtn . click ( ) ;
2023-11-02 02:56:01 +01:00
}
else {
2023-03-03 12:01:43 +01:00
this . _downloadFilesIndividually ( files ) ;
}
2023-01-17 10:41:50 +01:00
2023-03-03 12:01:43 +01:00
if ( ! canShare ) {
2023-07-06 21:29:36 +02:00
this . $downloadBtn . innerText = Localization . getTranslation ( "dialogs.download-again" ) ;
2023-01-17 10:41:50 +01:00
}
2023-07-06 21:29:36 +02:00
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.download-successful" , null , { descriptor : descriptor } ) ) ;
2023-11-24 16:25:30 +01:00
// Prevent clicking the button multiple times
2023-03-03 12:01:43 +01:00
this . $downloadBtn . style . pointerEvents = "none" ;
2023-11-02 01:22:41 +01:00
setTimeout ( ( ) => this . $downloadBtn . style . pointerEvents = "unset" , 2000 ) ;
2023-03-03 12:01:43 +01:00
} ;
2023-01-17 10:41:50 +01:00
2023-03-13 00:04:48 +01:00
document . title = files . length === 1
2023-07-06 21:29:36 +02:00
? ` ${ Localization . getTranslation ( "document-titles.file-received" ) } - PairDrop `
: ` ${ Localization . getTranslation ( "document-titles.file-received-plural" , null , { count : files . length } ) } - PairDrop ` ;
2023-10-11 23:22:10 +02:00
changeFavicon ( "images/favicon-96x96-notification.png" ) ;
2023-07-06 21:29:36 +02:00
2023-03-13 00:04:48 +01:00
Events . fire ( 'set-progress' , { peerId : peerId , progress : 1 , status : 'process' } )
this . show ( ) ;
2023-03-03 12:01:43 +01:00
2023-11-02 01:22:41 +01:00
setTimeout ( ( ) => {
2023-11-24 16:25:30 +01:00
// wait for the dialog to be shown
2023-03-03 12:01:43 +01:00
if ( canShare ) {
this . $shareBtn . click ( ) ;
2023-11-02 02:56:01 +01:00
}
else {
2023-03-03 12:01:43 +01:00
this . $downloadBtn . click ( ) ;
}
2023-03-13 00:04:48 +01:00
} , 500 ) ;
this . createPreviewElement ( files [ 0 ] )
. then ( canPreview => {
if ( canPreview ) {
console . log ( 'the file is able to preview' ) ;
2023-11-02 02:56:01 +01:00
}
else {
2023-03-13 00:04:48 +01:00
console . log ( 'the file is not able to preview' ) ;
}
} )
. catch ( r => console . error ( r ) ) ;
2023-01-17 10:41:50 +01:00
}
2023-03-03 12:01:43 +01:00
_downloadFilesIndividually ( files ) {
let tmpBtn = document . createElement ( "a" ) ;
for ( let i = 0 ; i < files . length ; i ++ ) {
tmpBtn . download = files [ i ] . name ;
tmpBtn . href = URL . createObjectURL ( files [ i ] ) ;
tmpBtn . click ( ) ;
}
}
2023-01-17 10:41:50 +01:00
hide ( ) {
super . hide ( ) ;
2023-11-24 16:25:30 +01:00
setTimeout ( async ( ) => {
this . $shareBtn . setAttribute ( 'hidden' , true ) ;
this . $downloadBtn . setAttribute ( 'disabled' , true ) ;
this . $previewBox . innerHTML = '' ;
this . _busy = false ;
await this . _nextFiles ( ) ;
} , 300 ) ;
2023-01-17 10:41:50 +01:00
}
}
class ReceiveRequestDialog extends ReceiveDialog {
2018-09-21 16:05:03 +02:00
2023-01-17 10:41:50 +01:00
constructor ( ) {
2023-03-01 10:44:57 +01:00
super ( 'receive-request-dialog' ) ;
2023-01-17 10:41:50 +01:00
2023-03-01 10:44:57 +01:00
this . $acceptRequestBtn = this . $el . querySelector ( '#accept-request' ) ;
this . $declineRequestBtn = this . $el . querySelector ( '#decline-request' ) ;
2023-01-17 10:41:50 +01:00
this . $acceptRequestBtn . addEventListener ( 'click' , _ => this . _respondToFileTransferRequest ( true ) ) ;
this . $declineRequestBtn . addEventListener ( 'click' , _ => this . _respondToFileTransferRequest ( false ) ) ;
Events . on ( 'files-transfer-request' , e => this . _onRequestFileTransfer ( e . detail . request , e . detail . peerId ) )
Events . on ( 'keydown' , e => this . _onKeyDown ( e ) ) ;
2023-02-08 12:55:28 +01:00
this . _filesTransferRequestQueue = [ ] ;
2023-01-17 10:41:50 +01:00
}
_onKeyDown ( e ) {
2023-11-24 16:25:30 +01:00
if ( ! this . isShown ( ) ) return ;
if ( e . code === "Escape" ) {
2023-02-10 03:26:08 +01:00
this . _respondToFileTransferRequest ( false ) ;
2020-12-20 21:30:28 +01:00
}
2023-01-17 10:41:50 +01:00
}
2023-01-10 16:03:52 +01:00
2023-01-17 10:41:50 +01:00
_onRequestFileTransfer ( request , peerId ) {
2023-02-08 12:55:28 +01:00
this . _filesTransferRequestQueue . push ( { request : request , peerId : peerId } ) ;
2023-05-09 03:21:10 +02:00
if ( this . isShown ( ) ) return ;
2023-02-08 12:55:28 +01:00
this . _dequeueRequests ( ) ;
}
_dequeueRequests ( ) {
if ( ! this . _filesTransferRequestQueue . length ) return ;
let { request , peerId } = this . _filesTransferRequestQueue . shift ( ) ;
this . _showRequestDialog ( request , peerId )
}
_showRequestDialog ( request , peerId ) {
2023-01-23 04:51:22 +01:00
this . correspondingPeerId = peerId ;
2023-01-10 16:03:52 +01:00
2023-03-03 12:01:43 +01:00
const displayName = $ ( peerId ) . ui . _displayName ( ) ;
2023-03-06 15:39:24 +01:00
const connectionHash = $ ( peerId ) . ui . _connectionHash ;
2023-09-13 18:15:01 +02:00
const badgeClassName = $ ( peerId ) . ui . _badgeClassName ( ) ;
this . _parseFileData ( displayName , connectionHash , request . header , request . imagesOnly , request . totalSize , badgeClassName ) ;
2023-01-17 10:41:50 +01:00
2023-03-13 14:21:26 +01:00
if ( request . thumbnailDataUrl && request . thumbnailDataUrl . substring ( 0 , 22 ) === "data:image/jpeg;base64" ) {
2023-01-17 10:41:50 +01:00
let element = document . createElement ( 'img' ) ;
element . src = request . thumbnailDataUrl ;
2023-01-10 16:03:52 +01:00
this . $previewBox . appendChild ( element )
2021-06-02 22:26:08 +05:30
}
2020-12-20 21:30:28 +01:00
2023-10-01 16:56:13 +02:00
const transferRequestTitle = request . imagesOnly
? Localization . getTranslation ( 'document-titles.image-transfer-requested' )
: Localization . getTranslation ( 'document-titles.file-transfer-requested' ) ;
2023-03-03 12:01:43 +01:00
2023-10-01 16:56:13 +02:00
this . $receiveTitle . innerText = transferRequestTitle ;
2023-10-06 02:57:46 +02:00
2023-10-01 16:56:13 +02:00
document . title = ` ${ transferRequestTitle } - PairDrop ` ;
2023-10-11 23:22:10 +02:00
changeFavicon ( "images/favicon-96x96-notification.png" ) ;
2023-11-24 16:25:30 +01:00
this . $acceptRequestBtn . removeAttribute ( 'disabled' ) ;
2023-02-10 03:26:08 +01:00
this . show ( ) ;
2018-09-21 16:05:03 +02:00
}
2023-01-17 10:41:50 +01:00
_respondToFileTransferRequest ( accepted ) {
Events . fire ( 'respond-to-files-transfer-request' , {
2023-01-23 04:51:22 +01:00
to : this . correspondingPeerId ,
2023-01-17 10:41:50 +01:00
accepted : accepted
} )
if ( accepted ) {
2023-01-23 04:51:22 +01:00
Events . fire ( 'set-progress' , { peerId : this . correspondingPeerId , progress : 0 , status : 'wait' } ) ;
2023-01-17 14:19:51 +01:00
NoSleepUI . enable ( ) ;
2018-09-21 16:05:03 +02:00
}
2023-02-08 12:55:28 +01:00
this . hide ( ) ;
2018-09-21 16:05:03 +02:00
}
hide ( ) {
2023-05-04 17:38:51 +02:00
// clear previewBox after dialog is closed
2023-11-24 16:25:30 +01:00
setTimeout ( ( ) => {
this . $previewBox . innerHTML = '' ;
this . $acceptRequestBtn . setAttribute ( 'disabled' , true ) ;
} , 300 ) ;
2023-05-04 17:38:51 +02:00
2018-09-21 16:05:03 +02:00
super . hide ( ) ;
2023-05-04 17:38:51 +02:00
// show next request
2023-11-24 16:25:30 +01:00
setTimeout ( ( ) => this . _dequeueRequests ( ) , 300 ) ;
2020-12-20 21:30:28 +01:00
}
2018-09-21 16:05:03 +02:00
}
2023-09-13 18:15:01 +02:00
class InputKeyContainer {
constructor ( inputKeyContainer , evaluationRegex , onAllCharsFilled , onNoAllCharsFilled , onLastCharFilled ) {
2023-01-10 05:07:57 +01:00
2023-09-13 18:15:01 +02:00
this . $inputKeyContainer = inputKeyContainer ;
this . $inputKeyChars = inputKeyContainer . querySelectorAll ( 'input' ) ;
2023-01-10 05:07:57 +01:00
2023-09-13 18:15:01 +02:00
this . $inputKeyChars . forEach ( char => char . addEventListener ( 'input' , e => this . _onCharsInput ( e ) ) ) ;
this . $inputKeyChars . forEach ( char => char . addEventListener ( 'keydown' , e => this . _onCharsKeyDown ( e ) ) ) ;
this . $inputKeyChars . forEach ( char => char . addEventListener ( 'keyup' , e => this . _onCharsKeyUp ( e ) ) ) ;
this . $inputKeyChars . forEach ( char => char . addEventListener ( 'focus' , e => e . target . select ( ) ) ) ;
this . $inputKeyChars . forEach ( char => char . addEventListener ( 'click' , e => e . target . select ( ) ) ) ;
2023-05-10 21:20:11 +02:00
2023-09-13 18:15:01 +02:00
this . evalRgx = evaluationRegex
this . _onAllCharsFilled = onAllCharsFilled ;
this . _onNotAllCharsFilled = onNoAllCharsFilled ;
this . _onLastCharFilled = onLastCharFilled ;
}
_enableChars ( ) {
2023-11-08 20:36:46 +01:00
this . $inputKeyChars . forEach ( char => char . removeAttribute ( 'disabled' ) ) ;
2023-09-13 18:15:01 +02:00
}
_disableChars ( ) {
2023-11-08 20:36:46 +01:00
this . $inputKeyChars . forEach ( char => char . setAttribute ( 'disabled' , true ) ) ;
2023-09-13 18:15:01 +02:00
}
_clearChars ( ) {
this . $inputKeyChars . forEach ( char => char . value = '' ) ;
}
_cleanUp ( ) {
this . _clearChars ( ) ;
this . _disableChars ( ) ;
2023-01-10 05:07:57 +01:00
}
_onCharsInput ( e ) {
2023-09-13 18:15:01 +02:00
if ( ! e . target . value . match ( this . evalRgx ) ) {
e . target . value = '' ;
return ;
}
this . _evaluateKeyChars ( ) ;
2023-01-17 10:11:17 +01:00
2023-01-10 05:07:57 +01:00
let nextSibling = e . target . nextElementSibling ;
if ( nextSibling ) {
e . preventDefault ( ) ;
nextSibling . focus ( ) ;
}
}
_onCharsKeyDown ( e ) {
let previousSibling = e . target . previousElementSibling ;
let nextSibling = e . target . nextElementSibling ;
if ( e . key === "Backspace" && previousSibling && ! e . target . value ) {
previousSibling . value = '' ;
previousSibling . focus ( ) ;
2023-11-02 02:56:01 +01:00
}
else if ( e . key === "ArrowRight" && nextSibling ) {
2023-01-10 05:07:57 +01:00
e . preventDefault ( ) ;
nextSibling . focus ( ) ;
2023-11-02 02:56:01 +01:00
}
else if ( e . key === "ArrowLeft" && previousSibling ) {
2023-01-10 05:07:57 +01:00
e . preventDefault ( ) ;
previousSibling . focus ( ) ;
}
}
2023-09-13 18:15:01 +02:00
_onCharsKeyUp ( e ) {
// deactivate submit btn when e.g. using backspace to clear element
if ( ! e . target . value ) {
this . _evaluateKeyChars ( ) ;
}
}
_getInputKey ( ) {
let key = "" ;
this . $inputKeyChars . forEach ( char => {
key += char . value ;
} )
return key ;
}
_onPaste ( pastedKey ) {
let rgx = new RegExp ( "(?!" + this . evalRgx . source + ")." , "g" ) ;
pastedKey = pastedKey . replace ( rgx , '' ) . substring ( 0 , this . $inputKeyChars . length )
for ( let i = 0 ; i < pastedKey . length ; i ++ ) {
document . activeElement . value = pastedKey . charAt ( i ) ;
2023-01-10 05:07:57 +01:00
let nextSibling = document . activeElement . nextElementSibling ;
if ( ! nextSibling ) break ;
nextSibling . focus ( ) ;
}
2023-09-13 18:15:01 +02:00
this . _evaluateKeyChars ( ) ;
2023-01-10 05:07:57 +01:00
}
2023-09-13 18:15:01 +02:00
_evaluateKeyChars ( ) {
if ( this . $inputKeyContainer . querySelectorAll ( 'input:placeholder-shown' ) . length > 0 ) {
this . _onNotAllCharsFilled ( ) ;
2023-11-02 02:56:01 +01:00
}
else {
2023-09-13 18:15:01 +02:00
this . _onAllCharsFilled ( ) ;
const lastCharFocused = document . activeElement === this . $inputKeyChars [ this . $inputKeyChars . length - 1 ] ;
if ( lastCharFocused ) {
this . _onLastCharFilled ( ) ;
2023-01-17 10:11:17 +01:00
}
2023-01-10 05:07:57 +01:00
}
}
2023-09-13 18:15:01 +02:00
focusLastChar ( ) {
let lastChar = this . $inputKeyChars [ this . $inputKeyChars . length - 1 ] ;
lastChar . focus ( ) ;
}
}
class PairDeviceDialog extends Dialog {
constructor ( ) {
super ( 'pair-device-dialog' ) ;
this . $pairDeviceHeaderBtn = $ ( 'pair-device' ) ;
this . $editPairedDevicesHeaderBtn = $ ( 'edit-paired-devices' ) ;
this . $footerInstructionsPairedDevices = $$ ( '.discovery-wrapper .badge-room-secret' ) ;
this . $key = this . $el . querySelector ( '.key' ) ;
this . $qrCode = this . $el . querySelector ( '.key-qr-code' ) ;
this . $form = this . $el . querySelector ( 'form' ) ;
this . $closeBtn = this . $el . querySelector ( '[close]' )
this . $pairSubmitBtn = this . $el . querySelector ( 'button[type="submit"]' ) ;
this . inputKeyContainer = new InputKeyContainer (
this . $el . querySelector ( '.input-key-container' ) ,
/\d/ ,
2023-11-08 20:36:46 +01:00
( ) => this . $pairSubmitBtn . removeAttribute ( 'disabled' ) ,
( ) => this . $pairSubmitBtn . setAttribute ( 'disabled' , true ) ,
2023-09-13 18:15:01 +02:00
( ) => this . _submit ( )
) ;
this . $pairDeviceHeaderBtn . addEventListener ( 'click' , _ => this . _pairDeviceInitiate ( ) ) ;
this . $form . addEventListener ( 'submit' , e => this . _onSubmit ( e ) ) ;
this . $closeBtn . addEventListener ( 'click' , _ => this . _close ( ) ) ;
Events . on ( 'keydown' , e => this . _onKeyDown ( e ) ) ;
Events . on ( 'ws-disconnected' , _ => this . hide ( ) ) ;
Events . on ( 'pair-device-initiated' , e => this . _onPairDeviceInitiated ( e . detail ) ) ;
Events . on ( 'pair-device-joined' , e => this . _onPairDeviceJoined ( e . detail . peerId , e . detail . roomSecret ) ) ;
Events . on ( 'peers' , e => this . _onPeers ( e . detail ) ) ;
Events . on ( 'peer-joined' , e => this . _onPeerJoined ( e . detail ) ) ;
Events . on ( 'pair-device-join-key-invalid' , _ => this . _onPublicRoomJoinKeyInvalid ( ) ) ;
Events . on ( 'pair-device-canceled' , e => this . _onPairDeviceCanceled ( e . detail ) ) ;
Events . on ( 'evaluate-number-room-secrets' , _ => this . _evaluateNumberRoomSecrets ( ) )
Events . on ( 'secret-room-deleted' , e => this . _onSecretRoomDeleted ( e . detail ) ) ;
this . $el . addEventListener ( 'paste' , e => this . _onPaste ( e ) ) ;
2023-10-11 20:19:19 +02:00
this . $qrCode . addEventListener ( 'click' , _ => this . _copyPairUrl ( ) ) ;
2023-09-13 18:15:01 +02:00
this . pairPeer = { } ;
}
_onKeyDown ( e ) {
2023-11-24 16:25:30 +01:00
if ( ! this . isShown ( ) ) return ;
if ( e . code === "Escape" ) {
// Timeout to prevent share mode from getting cancelled simultaneously
2023-11-02 01:22:41 +01:00
setTimeout ( ( ) => this . _close ( ) , 50 ) ;
2023-09-13 18:15:01 +02:00
}
}
_onPaste ( e ) {
e . preventDefault ( ) ;
2023-11-02 02:56:01 +01:00
let pastedKey = e . clipboardData
. getData ( "Text" )
. replace ( /\D/g , '' )
. substring ( 0 , 6 ) ;
2023-09-13 18:15:01 +02:00
this . inputKeyContainer . _onPaste ( pastedKey ) ;
}
2023-01-10 05:07:57 +01:00
_pairDeviceInitiate ( ) {
Events . fire ( 'pair-device-initiate' ) ;
}
2023-09-13 18:15:01 +02:00
_onPairDeviceInitiated ( msg ) {
this . pairKey = msg . pairKey ;
2023-01-10 05:07:57 +01:00
this . roomSecret = msg . roomSecret ;
2023-12-05 19:26:17 +01:00
this . _setKeyAndQRCode ( ) ;
this . inputKeyContainer . _enableChars ( ) ;
this . show ( ) ;
}
_setKeyAndQRCode ( ) {
2023-09-13 18:15:01 +02:00
this . $key . innerText = ` ${ this . pairKey . substring ( 0 , 3 ) } ${ this . pairKey . substring ( 3 , 6 ) } `
2023-12-05 19:26:17 +01:00
2023-01-10 05:07:57 +01:00
// Display the QR code for the url
const qr = new QRCode ( {
2023-10-11 20:19:19 +02:00
content : this . _getPairUrl ( ) ,
2023-11-20 05:23:21 +01:00
width : 130 ,
height : 130 ,
2023-10-23 19:39:33 +02:00
padding : 1 ,
2023-11-29 18:53:57 +01:00
background : 'white' ,
2023-10-23 19:39:33 +02:00
color : 'rgb(18, 18, 18)' ,
2023-01-10 05:07:57 +01:00
ecl : "L" ,
join : true
} ) ;
this . $qrCode . innerHTML = qr . svg ( ) ;
}
2023-10-11 20:19:19 +02:00
_getPairUrl ( ) {
2023-01-10 05:07:57 +01:00
let url = new URL ( location . href ) ;
2023-09-13 18:15:01 +02:00
url . searchParams . append ( 'pair_key' , this . pairKey )
2023-01-10 05:07:57 +01:00
return url . href ;
}
2023-10-11 20:19:19 +02:00
_copyPairUrl ( ) {
navigator . clipboard . writeText ( this . _getPairUrl ( ) )
. then ( _ => {
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.pair-url-copied-to-clipboard" ) ) ;
} )
. catch ( _ => {
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.copied-to-clipboard-error" ) ) ;
} )
}
2023-03-02 16:30:47 +01:00
_onSubmit ( e ) {
e . preventDefault ( ) ;
2023-09-13 18:15:01 +02:00
this . _submit ( ) ;
}
_submit ( ) {
let inputKey = this . inputKeyContainer . _getInputKey ( ) ;
this . _pairDeviceJoin ( inputKey ) ;
2023-01-10 05:07:57 +01:00
}
2023-09-13 18:15:01 +02:00
_pairDeviceJoin ( pairKey ) {
if ( /^\d{6}$/g . test ( pairKey ) ) {
Events . fire ( 'pair-device-join' , pairKey ) ;
this . inputKeyContainer . focusLastChar ( ) ;
2023-01-10 05:07:57 +01:00
}
}
2023-09-13 18:15:01 +02:00
_onPairDeviceJoined ( peerId , roomSecret ) {
// abort if peer is another tab on the same browser and remove room-type from gui
2023-05-04 17:38:51 +02:00
if ( BrowserTabsConnector . peerIsSameBrowser ( peerId ) ) {
2023-02-08 04:06:42 +01:00
this . _cleanUp ( ) ;
2023-05-10 21:20:11 +02:00
this . hide ( ) ;
2023-09-13 18:15:01 +02:00
Events . fire ( 'room-secrets-deleted' , [ roomSecret ] ) ;
2023-07-06 21:29:36 +02:00
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.pairing-tabs-error" ) ) ;
2023-05-04 17:38:51 +02:00
return ;
}
2023-05-10 21:20:11 +02:00
// save pairPeer and wait for it to connect to ensure both devices have gotten the roomSecret
this . pairPeer = {
"peerId" : peerId ,
"roomSecret" : roomSecret
} ;
}
_onPeers ( message ) {
message . peers . forEach ( messagePeer => {
2023-09-13 18:15:01 +02:00
this . _evaluateJoinedPeer ( messagePeer . id , message . roomType , message . roomId ) ;
2023-05-10 21:20:11 +02:00
} ) ;
}
_onPeerJoined ( message ) {
2023-09-13 18:15:01 +02:00
this . _evaluateJoinedPeer ( message . peer . id , message . roomType , message . roomId ) ;
2023-05-10 21:20:11 +02:00
}
2023-09-13 18:15:01 +02:00
_evaluateJoinedPeer ( peerId , roomType , roomId ) {
const noPairPeerSaved = ! Object . keys ( this . pairPeer ) ;
if ( ! peerId || ! roomType || ! roomId || noPairPeerSaved ) return ;
2023-05-10 21:20:11 +02:00
const samePeerId = peerId === this . pairPeer . peerId ;
2023-09-13 18:15:01 +02:00
const sameRoomSecret = roomId === this . pairPeer . roomSecret ;
const typeIsSecret = roomType === "secret" ;
2023-05-10 21:20:11 +02:00
2023-09-13 18:15:01 +02:00
if ( ! samePeerId || ! sameRoomSecret || ! typeIsSecret ) return ;
2023-05-10 21:20:11 +02:00
2023-09-13 18:15:01 +02:00
this . _onPairPeerJoined ( peerId , roomId ) ;
2023-05-10 21:20:11 +02:00
this . pairPeer = { } ;
}
_onPairPeerJoined ( peerId , roomSecret ) {
2023-05-04 17:38:51 +02:00
// if devices are paired that are already connected we must save the names at this point
const $peer = $ ( peerId ) ;
let displayName , deviceName ;
if ( $peer ) {
displayName = $peer . ui . _peer . name . displayName ;
deviceName = $peer . ui . _peer . name . deviceName ;
}
2023-11-02 02:56:01 +01:00
PersistentStorage
. addRoomSecret ( roomSecret , displayName , deviceName )
2023-05-04 17:38:51 +02:00
. then ( _ => {
2023-07-06 21:29:36 +02:00
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.pairing-success" ) ) ;
2023-05-04 17:38:51 +02:00
this . _evaluateNumberRoomSecrets ( ) ;
} )
2023-11-02 01:22:41 +01:00
. finally ( ( ) => {
2023-05-04 17:38:51 +02:00
this . _cleanUp ( ) ;
2023-05-10 21:20:11 +02:00
this . hide ( ) ;
2023-05-04 17:38:51 +02:00
} )
. catch ( _ => {
2023-07-06 21:29:36 +02:00
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.pairing-not-persistent" ) ) ;
2023-05-04 17:38:51 +02:00
PersistentStorage . logBrowserNotCapable ( ) ;
} ) ;
2023-01-10 05:07:57 +01:00
}
2023-09-13 18:15:01 +02:00
_onPublicRoomJoinKeyInvalid ( ) {
2023-07-06 21:29:36 +02:00
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.pairing-key-invalid" ) ) ;
2023-01-10 05:07:57 +01:00
}
2023-09-13 18:15:01 +02:00
_close ( ) {
this . _pairDeviceCancel ( ) ;
}
2023-01-10 05:07:57 +01:00
_pairDeviceCancel ( ) {
this . hide ( ) ;
this . _cleanUp ( ) ;
Events . fire ( 'pair-device-cancel' ) ;
}
2023-09-13 18:15:01 +02:00
_onPairDeviceCanceled ( pairKey ) {
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.pairing-key-invalidated" , null , { key : pairKey } ) ) ;
2023-01-10 05:07:57 +01:00
}
_cleanUp ( ) {
this . roomSecret = null ;
2023-09-13 18:15:01 +02:00
this . pairKey = null ;
this . inputKeyContainer . _cleanUp ( ) ;
2023-05-10 21:20:11 +02:00
this . pairPeer = { } ;
2023-01-10 05:07:57 +01:00
}
_onSecretRoomDeleted ( roomSecret ) {
2023-11-02 02:56:01 +01:00
PersistentStorage
. deleteRoomSecret ( roomSecret )
. then ( _ => {
this . _evaluateNumberRoomSecrets ( ) ;
} ) ;
2023-01-10 05:07:57 +01:00
}
_evaluateNumberRoomSecrets ( ) {
2023-11-02 02:56:01 +01:00
PersistentStorage
. getAllRoomSecrets ( )
2023-10-11 23:22:10 +02:00
. then ( roomSecrets => {
if ( roomSecrets . length > 0 ) {
this . $editPairedDevicesHeaderBtn . removeAttribute ( 'hidden' ) ;
this . $footerInstructionsPairedDevices . removeAttribute ( 'hidden' ) ;
2023-11-02 02:56:01 +01:00
}
else {
2023-11-08 20:36:46 +01:00
this . $editPairedDevicesHeaderBtn . setAttribute ( 'hidden' , true ) ;
this . $footerInstructionsPairedDevices . setAttribute ( 'hidden' , true ) ;
2023-10-11 23:22:10 +02:00
}
Events . fire ( 'evaluate-footer-badges' ) ;
} ) ;
2023-01-10 05:07:57 +01:00
}
}
2023-05-04 17:38:51 +02:00
class EditPairedDevicesDialog extends Dialog {
2023-01-10 05:07:57 +01:00
constructor ( ) {
2023-05-04 17:38:51 +02:00
super ( 'edit-paired-devices-dialog' ) ;
this . $pairedDevicesWrapper = this . $el . querySelector ( '.paired-devices-wrapper' ) ;
2023-10-11 23:22:10 +02:00
this . $footerBadgePairedDevices = $$ ( '.discovery-wrapper .badge-room-secret' ) ;
2023-09-13 18:15:01 +02:00
2023-05-04 17:38:51 +02:00
$ ( 'edit-paired-devices' ) . addEventListener ( 'click' , _ => this . _onEditPairedDevices ( ) ) ;
2023-10-11 23:22:10 +02:00
this . $footerBadgePairedDevices . addEventListener ( 'click' , _ => this . _onEditPairedDevices ( ) ) ;
2023-05-04 17:38:51 +02:00
Events . on ( 'peer-display-name-changed' , e => this . _onPeerDisplayNameChanged ( e ) ) ;
Events . on ( 'keydown' , e => this . _onKeyDown ( e ) ) ;
2023-01-10 05:07:57 +01:00
}
2023-05-04 17:38:51 +02:00
_onKeyDown ( e ) {
2023-11-24 16:25:30 +01:00
if ( ! this . isShown ( ) ) return ;
if ( e . code === "Escape" ) {
2023-05-04 17:38:51 +02:00
this . hide ( ) ;
}
2023-01-10 05:07:57 +01:00
}
2023-05-04 17:38:51 +02:00
async _initDOM ( ) {
2023-12-09 17:37:00 +01:00
const pairedDeviceRemovedString = Localization . getTranslation ( "dialogs.paired-device-removed" ) ;
2023-09-14 18:15:07 +02:00
const unpairString = Localization . getTranslation ( "dialogs.unpair" ) . toUpperCase ( ) ;
const autoAcceptString = Localization . getTranslation ( "dialogs.auto-accept" ) . toLowerCase ( ) ;
2023-05-04 17:38:51 +02:00
const roomSecretsEntries = await PersistentStorage . getAllRoomSecretEntries ( ) ;
2023-09-14 18:15:07 +02:00
2023-11-02 02:56:01 +01:00
roomSecretsEntries
. forEach ( roomSecretsEntry => {
let $pairedDevice = document . createElement ( 'div' ) ;
2023-11-29 23:39:55 +01:00
$pairedDevice . classList . add ( "paired-device" ) ;
2023-12-09 17:37:00 +01:00
$pairedDevice . setAttribute ( 'placeholder' , pairedDeviceRemovedString ) ;
2023-05-04 17:38:51 +02:00
2023-11-02 02:56:01 +01:00
$pairedDevice . innerHTML = `
2023-11-29 23:39:55 +01:00
< div class = "display-name" >
< span class = "fw" >
$ { roomSecretsEntry . display _name }
< / s p a n >
< / d i v >
< div class = "device-name" >
< span class = "fw" >
$ { roomSecretsEntry . device _name }
< / s p a n >
< / d i v >
2023-12-09 17:37:00 +01:00
< div class = "button-wrapper row fw center wrap" >
< div class = "center grow" >
< span class = "center wrap" >
2023-11-29 23:39:55 +01:00
$ { autoAcceptString }
< / s p a n >
2023-12-12 13:39:55 +01:00
< label class = "auto-accept switch pointer m-1" >
2023-11-29 23:39:55 +01:00
< input type = "checkbox" $ { roomSecretsEntry . auto _accept ? "checked" : "" } >
< div class = "slider round" > < / d i v >
< / l a b e l >
< / d i v >
2023-12-09 17:37:00 +01:00
< button class = "btn grow" type = "button" > $ { unpairString } < / b u t t o n >
2023-11-29 23:39:55 +01:00
< / d i v > `
2023-11-02 02:56:01 +01:00
$pairedDevice
. querySelector ( 'input[type="checkbox"]' )
. addEventListener ( 'click' , e => {
2023-11-09 03:48:17 +01:00
PersistentStorage
. updateRoomSecretAutoAccept ( roomSecretsEntry . secret , e . target . checked )
2023-11-02 02:56:01 +01:00
. then ( roomSecretsEntry => {
Events . fire ( 'auto-accept-updated' , {
'roomSecret' : roomSecretsEntry . entry . secret ,
'autoAccept' : e . target . checked
} ) ;
} ) ;
} ) ;
2023-05-04 17:38:51 +02:00
2023-11-02 02:56:01 +01:00
$pairedDevice
. querySelector ( 'button' )
. addEventListener ( 'click' , e => {
2023-11-09 03:48:17 +01:00
PersistentStorage
. deleteRoomSecret ( roomSecretsEntry . secret )
2023-11-02 02:56:01 +01:00
. then ( roomSecret => {
Events . fire ( 'room-secrets-deleted' , [ roomSecret ] ) ;
Events . fire ( 'evaluate-number-room-secrets' ) ;
2023-12-09 17:37:00 +01:00
$pairedDevice . innerText = "" ;
2023-11-02 02:56:01 +01:00
} ) ;
} )
2023-05-04 17:38:51 +02:00
2023-11-02 02:56:01 +01:00
this . $pairedDevicesWrapper . appendChild ( $pairedDevice )
} )
2023-05-04 17:38:51 +02:00
}
hide ( ) {
super . hide ( ) ;
2023-11-02 01:22:41 +01:00
setTimeout ( ( ) => {
2023-05-04 17:38:51 +02:00
this . $pairedDevicesWrapper . innerHTML = ""
} , 300 ) ;
}
_onEditPairedDevices ( ) {
2023-11-20 05:23:21 +01:00
this . _initDOM ( )
. then ( _ => {
2023-11-24 16:25:30 +01:00
this . _evaluateOverflowing ( this . $pairedDevicesWrapper ) ;
2023-11-20 05:23:21 +01:00
this . show ( ) ;
} ) ;
}
2023-03-02 16:30:47 +01:00
_clearRoomSecrets ( ) {
2023-11-02 02:56:01 +01:00
PersistentStorage
. getAllRoomSecrets ( )
2023-05-04 17:38:51 +02:00
. then ( roomSecrets => {
2023-11-02 02:56:01 +01:00
PersistentStorage
. clearRoomSecrets ( )
. finally ( ( ) => {
Events . fire ( 'room-secrets-deleted' , roomSecrets ) ;
Events . fire ( 'evaluate-number-room-secrets' ) ;
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.pairing-cleared" ) ) ;
this . hide ( ) ;
} )
2023-05-04 17:38:51 +02:00
} ) ;
}
_onPeerDisplayNameChanged ( e ) {
const peerId = e . detail . peerId ;
const peerNode = $ ( peerId ) ;
if ( ! peerNode ) return ;
const peer = peerNode . ui . _peer ;
2023-09-13 18:15:01 +02:00
if ( ! peer || ! peer . _roomIds [ "secret" ] ) return ;
2023-05-04 17:38:51 +02:00
2023-11-02 02:56:01 +01:00
PersistentStorage
. updateRoomSecretNames ( peer . _roomIds [ "secret" ] , peer . name . displayName , peer . name . deviceName )
. then ( roomSecretEntry => {
console . log ( ` Successfully updated DisplayName and DeviceName for roomSecretEntry ${ roomSecretEntry . key } ` ) ;
} )
2023-01-10 05:07:57 +01:00
}
}
2023-09-13 18:15:01 +02:00
class PublicRoomDialog extends Dialog {
constructor ( ) {
super ( 'public-room-dialog' ) ;
this . $key = this . $el . querySelector ( '.key' ) ;
this . $qrCode = this . $el . querySelector ( '.key-qr-code' ) ;
this . $form = this . $el . querySelector ( 'form' ) ;
this . $closeBtn = this . $el . querySelector ( '[close]' ) ;
this . $leaveBtn = this . $el . querySelector ( '.leave-room' ) ;
this . $joinSubmitBtn = this . $el . querySelector ( 'button[type="submit"]' ) ;
this . $headerBtnJoinPublicRoom = $ ( 'join-public-room' ) ;
2023-10-11 23:22:10 +02:00
this . $footerBadgePublicRoomDevices = $$ ( '.discovery-wrapper .badge-room-public-id' ) ;
2023-09-13 18:15:01 +02:00
this . $form . addEventListener ( 'submit' , e => this . _onSubmit ( e ) ) ;
this . $closeBtn . addEventListener ( 'click' , _ => this . hide ( ) ) ;
this . $leaveBtn . addEventListener ( 'click' , _ => this . _leavePublicRoom ( ) )
this . $headerBtnJoinPublicRoom . addEventListener ( 'click' , _ => this . _onHeaderBtnClick ( ) ) ;
2023-10-11 23:22:10 +02:00
this . $footerBadgePublicRoomDevices . addEventListener ( 'click' , _ => this . _onHeaderBtnClick ( ) ) ;
2023-09-13 18:15:01 +02:00
this . inputKeyContainer = new InputKeyContainer (
this . $el . querySelector ( '.input-key-container' ) ,
/[a-z|A-Z]/ ,
2023-11-08 20:36:46 +01:00
( ) => this . $joinSubmitBtn . removeAttribute ( 'disabled' ) ,
( ) => this . $joinSubmitBtn . setAttribute ( 'disabled' , true ) ,
2023-09-13 18:15:01 +02:00
( ) => this . _submit ( )
) ;
Events . on ( 'keydown' , e => this . _onKeyDown ( e ) ) ;
Events . on ( 'public-room-created' , e => this . _onPublicRoomCreated ( e . detail ) ) ;
Events . on ( 'peers' , e => this . _onPeers ( e . detail ) ) ;
Events . on ( 'peer-joined' , e => this . _onPeerJoined ( e . detail ) ) ;
Events . on ( 'public-room-id-invalid' , e => this . _onPublicRoomIdInvalid ( e . detail ) ) ;
Events . on ( 'public-room-left' , _ => this . _onPublicRoomLeft ( ) ) ;
this . $el . addEventListener ( 'paste' , e => this . _onPaste ( e ) ) ;
2023-10-11 20:19:19 +02:00
this . $qrCode . addEventListener ( 'click' , _ => this . _copyShareRoomUrl ( ) ) ;
2023-09-13 18:15:01 +02:00
Events . on ( 'ws-connected' , _ => this . _onWsConnected ( ) ) ;
2023-09-14 20:06:17 +02:00
Events . on ( 'translation-loaded' , _ => this . setFooterBadge ( ) ) ;
2023-09-13 18:15:01 +02:00
}
_onKeyDown ( e ) {
2023-11-24 16:25:30 +01:00
if ( ! this . isShown ( ) ) return ;
if ( e . code === "Escape" ) {
2023-09-13 18:15:01 +02:00
this . hide ( ) ;
}
}
_onPaste ( e ) {
e . preventDefault ( ) ;
let pastedKey = e . clipboardData . getData ( "Text" ) ;
this . inputKeyContainer . _onPaste ( pastedKey ) ;
}
_onHeaderBtnClick ( ) {
if ( this . roomId ) {
this . show ( ) ;
2023-11-02 02:56:01 +01:00
}
else {
2023-09-13 18:15:01 +02:00
this . _createPublicRoom ( ) ;
}
}
_createPublicRoom ( ) {
Events . fire ( 'create-public-room' ) ;
}
_onPublicRoomCreated ( roomId ) {
this . roomId = roomId ;
2023-12-05 19:26:17 +01:00
this . _setKeyAndQrCode ( ) ;
2023-09-13 18:15:01 +02:00
this . show ( ) ;
sessionStorage . setItem ( 'public_room_id' , roomId ) ;
}
2023-12-05 19:26:17 +01:00
_setKeyAndQrCode ( ) {
2023-09-14 20:06:17 +02:00
if ( ! this . roomId ) return ;
2023-09-13 18:15:01 +02:00
this . $key . innerText = this . roomId . toUpperCase ( ) ;
// Display the QR code for the url
const qr = new QRCode ( {
2023-10-11 20:19:19 +02:00
content : this . _getShareRoomUrl ( ) ,
2023-11-20 05:23:21 +01:00
width : 130 ,
height : 130 ,
2023-10-23 19:39:33 +02:00
padding : 1 ,
2023-11-29 18:53:57 +01:00
background : 'white' ,
2023-10-23 19:39:33 +02:00
color : 'rgb(18, 18, 18)' ,
2023-09-13 18:15:01 +02:00
ecl : "L" ,
join : true
} ) ;
this . $qrCode . innerHTML = qr . svg ( ) ;
2023-09-14 20:06:17 +02:00
this . setFooterBadge ( ) ;
}
setFooterBadge ( ) {
if ( ! this . roomId ) return ;
2023-10-11 23:22:10 +02:00
this . $footerBadgePublicRoomDevices . innerText = Localization . getTranslation ( "footer.public-room-devices" , null , {
2023-09-13 18:15:01 +02:00
roomId : this . roomId . toUpperCase ( )
} ) ;
2023-10-11 23:22:10 +02:00
this . $footerBadgePublicRoomDevices . removeAttribute ( 'hidden' ) ;
2023-09-14 20:06:17 +02:00
2023-10-11 23:22:10 +02:00
Events . fire ( 'evaluate-footer-badges' ) ;
2023-09-13 18:15:01 +02:00
}
2023-10-11 20:19:19 +02:00
_getShareRoomUrl ( ) {
2023-09-13 18:15:01 +02:00
let url = new URL ( location . href ) ;
2023-10-11 20:19:19 +02:00
url . searchParams . append ( 'room_id' , this . roomId )
2023-09-13 18:15:01 +02:00
return url . href ;
}
2023-10-11 20:19:19 +02:00
_copyShareRoomUrl ( ) {
navigator . clipboard . writeText ( this . _getShareRoomUrl ( ) )
. then ( _ => {
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.room-url-copied-to-clipboard" ) ) ;
} )
. catch ( _ => {
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.copied-to-clipboard-error" ) ) ;
} )
}
2023-09-13 18:15:01 +02:00
_onWsConnected ( ) {
let roomId = sessionStorage . getItem ( 'public_room_id' ) ;
if ( ! roomId ) return ;
this . roomId = roomId ;
2023-12-05 19:26:17 +01:00
this . _setKeyAndQrCode ( ) ;
2023-09-13 18:15:01 +02:00
this . _joinPublicRoom ( roomId , true ) ;
}
_onSubmit ( e ) {
e . preventDefault ( ) ;
this . _submit ( ) ;
}
_submit ( ) {
let inputKey = this . inputKeyContainer . _getInputKey ( ) ;
this . _joinPublicRoom ( inputKey ) ;
}
_joinPublicRoom ( roomId , createIfInvalid = false ) {
roomId = roomId . toLowerCase ( ) ;
if ( /^[a-z]{5}$/g . test ( roomId ) ) {
this . roomIdJoin = roomId ;
this . inputKeyContainer . focusLastChar ( ) ;
Events . fire ( 'join-public-room' , {
roomId : roomId ,
createIfInvalid : createIfInvalid
} ) ;
}
}
_onPeers ( message ) {
message . peers . forEach ( messagePeer => {
this . _evaluateJoinedPeer ( messagePeer . id , message . roomId ) ;
} ) ;
}
_onPeerJoined ( message ) {
this . _evaluateJoinedPeer ( message . peer . id , message . roomId ) ;
}
_evaluateJoinedPeer ( peerId , roomId ) {
const isInitiatedRoomId = roomId === this . roomId ;
const isJoinedRoomId = roomId === this . roomIdJoin ;
if ( ! peerId || ! roomId || ! ( isInitiatedRoomId || isJoinedRoomId ) ) return ;
this . hide ( ) ;
sessionStorage . setItem ( 'public_room_id' , roomId ) ;
if ( isJoinedRoomId ) {
this . roomId = roomId ;
this . roomIdJoin = false ;
2023-12-05 19:26:17 +01:00
this . _setKeyAndQrCode ( ) ;
2023-09-13 18:15:01 +02:00
}
}
_onPublicRoomIdInvalid ( roomId ) {
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.public-room-id-invalid" ) ) ;
if ( roomId === sessionStorage . getItem ( 'public_room_id' ) ) {
sessionStorage . removeItem ( 'public_room_id' ) ;
}
}
_leavePublicRoom ( ) {
Events . fire ( 'leave-public-room' , this . roomId ) ;
}
_onPublicRoomLeft ( ) {
let publicRoomId = this . roomId . toUpperCase ( ) ;
this . hide ( ) ;
this . _cleanUp ( ) ;
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.public-room-left" , null , { publicRoomId : publicRoomId } ) ) ;
}
show ( ) {
this . inputKeyContainer . _enableChars ( ) ;
super . show ( ) ;
}
hide ( ) {
this . inputKeyContainer . _cleanUp ( ) ;
super . hide ( ) ;
}
_cleanUp ( ) {
this . roomId = null ;
this . inputKeyContainer . _cleanUp ( ) ;
sessionStorage . removeItem ( 'public_room_id' ) ;
2023-11-08 20:36:46 +01:00
this . $footerBadgePublicRoomDevices . setAttribute ( 'hidden' , true ) ;
2023-10-11 23:22:10 +02:00
Events . fire ( 'evaluate-footer-badges' ) ;
2023-09-13 18:15:01 +02:00
}
}
2018-09-21 16:05:03 +02:00
class SendTextDialog extends Dialog {
constructor ( ) {
2023-03-01 10:44:57 +01:00
super ( 'send-text-dialog' ) ;
2023-11-24 16:25:30 +01:00
this . $text = this . $el . querySelector ( '.textarea' ) ;
2023-03-03 12:01:43 +01:00
this . $peerDisplayName = this . $el . querySelector ( '.display-name' ) ;
2023-02-10 03:26:08 +01:00
this . $form = this . $el . querySelector ( 'form' ) ;
this . $submit = this . $el . querySelector ( 'button[type="submit"]' ) ;
2023-03-02 16:30:47 +01:00
this . $form . addEventListener ( 'submit' , e => this . _onSubmit ( e ) ) ;
2023-11-24 16:25:30 +01:00
this . $text . addEventListener ( 'input' , _ => this . _onInput ( ) ) ;
2024-02-01 00:07:41 +01:00
this . $text . addEventListener ( 'paste' , e => this . _onPaste ( e ) ) ;
this . $text . addEventListener ( 'drop' , e => this . _onDrop ( e ) ) ;
2023-11-24 16:25:30 +01:00
Events . on ( 'text-recipient' , e => this . _onRecipient ( e . detail . peerId , e . detail . deviceName ) ) ;
2023-09-14 20:06:17 +02:00
Events . on ( 'keydown' , e => this . _onKeyDown ( e ) ) ;
2022-12-30 20:34:54 +01:00
}
2023-11-24 16:25:30 +01:00
_onKeyDown ( e ) {
2023-05-27 01:13:49 +02:00
if ( ! this . isShown ( ) ) return ;
if ( e . code === "Escape" ) {
this . hide ( ) ;
2023-11-02 02:56:01 +01:00
}
else if ( e . code === "Enter" && ( e . ctrlKey || e . metaKey ) ) {
2023-11-24 16:25:30 +01:00
if ( this . _textEmpty ( ) ) return ;
2023-05-27 01:13:49 +02:00
this . _send ( ) ;
2022-12-30 20:34:54 +01:00
}
2018-09-21 16:05:03 +02:00
}
2024-02-01 00:07:41 +01:00
async _onDrop ( e ) {
e . preventDefault ( )
const text = e . dataTransfer . getData ( "text" ) ;
const selection = window . getSelection ( ) ;
if ( selection . rangeCount ) {
selection . deleteFromDocument ( ) ;
selection . getRangeAt ( 0 ) . insertNode ( document . createTextNode ( text ) ) ;
}
this . _onInput ( ) ;
}
async _onPaste ( e ) {
e . preventDefault ( )
const text = ( e . clipboardData || window . clipboardData ) . getData ( 'text' ) ;
const selection = window . getSelection ( ) ;
if ( selection . rangeCount ) {
selection . deleteFromDocument ( ) ;
const textNode = document . createTextNode ( text ) ;
const range = document . createRange ( ) ;
range . setStart ( textNode , textNode . length ) ;
range . collapse ( true ) ;
selection . getRangeAt ( 0 ) . insertNode ( textNode ) ;
selection . removeAllRanges ( ) ;
selection . addRange ( range ) ;
}
this . _onInput ( ) ;
}
2023-11-24 16:25:30 +01:00
_textEmpty ( ) {
2023-10-11 18:56:07 +02:00
return ! this . $text . innerText || this . $text . innerText === "\n" ;
2023-02-10 03:26:08 +01:00
}
2023-11-24 16:25:30 +01:00
_onInput ( ) {
if ( this . _textEmpty ( ) ) {
2023-11-08 20:36:46 +01:00
this . $submit . setAttribute ( 'disabled' , true ) ;
2023-11-20 05:27:22 +01:00
// remove remaining whitespace on Firefox on text deletion
this . $text . innerText = "" ;
2023-11-02 02:56:01 +01:00
}
else {
2023-02-10 03:26:08 +01:00
this . $submit . removeAttribute ( 'disabled' ) ;
}
2023-11-24 16:25:30 +01:00
this . _evaluateOverflowing ( this . $text ) ;
2023-02-10 03:26:08 +01:00
}
2023-03-01 10:04:37 +01:00
_onRecipient ( peerId , deviceName ) {
2023-01-23 04:51:22 +01:00
this . correspondingPeerId = peerId ;
2023-03-01 10:04:37 +01:00
this . $peerDisplayName . innerText = deviceName ;
2025-02-13 10:24:27 +01:00
this . $peerDisplayName . classList . remove ( "badge-room-ip" , "badge-room-secret" , "badge-room-public-id" ) ;
2023-09-13 18:15:01 +02:00
this . $peerDisplayName . classList . add ( $ ( peerId ) . ui . _badgeClassName ( ) ) ;
2018-09-21 16:05:03 +02:00
this . show ( ) ;
2020-12-22 21:23:10 +01:00
const range = document . createRange ( ) ;
const sel = window . getSelection ( ) ;
2023-01-17 10:47:44 +01:00
range . selectNodeContents ( this . $text ) ;
sel . removeAllRanges ( ) ;
sel . addRange ( range ) ;
2018-09-21 16:05:03 +02:00
}
2023-03-02 16:30:47 +01:00
_onSubmit ( e ) {
e . preventDefault ( ) ;
this . _send ( ) ;
}
2023-01-14 01:43:44 +01:00
_send ( ) {
2018-09-21 16:05:03 +02:00
Events . fire ( 'send-text' , {
2023-01-23 04:51:22 +01:00
to : this . correspondingPeerId ,
2023-10-11 18:56:07 +02:00
text : this . $text . innerText
2018-09-21 16:05:03 +02:00
} ) ;
2023-02-10 03:26:08 +01:00
this . hide ( ) ;
2023-11-20 03:55:00 +01:00
setTimeout ( ( ) => this . $text . innerText = "" , 300 ) ;
2018-09-21 16:05:03 +02:00
}
}
class ReceiveTextDialog extends Dialog {
constructor ( ) {
2023-03-01 10:44:57 +01:00
super ( 'receive-text-dialog' ) ;
2023-02-10 03:26:08 +01:00
Events . on ( 'text-received' , e => this . _onText ( e . detail . text , e . detail . peerId ) ) ;
2018-09-21 16:05:03 +02:00
this . $text = this . $el . querySelector ( '#text' ) ;
2023-02-10 03:26:08 +01:00
this . $copy = this . $el . querySelector ( '#copy' ) ;
this . $close = this . $el . querySelector ( '#close' ) ;
this . $copy . addEventListener ( 'click' , _ => this . _onCopy ( ) ) ;
this . $close . addEventListener ( 'click' , _ => this . hide ( ) ) ;
2023-09-14 20:06:17 +02:00
Events . on ( 'keydown' , e => this . _onKeyDown ( e ) ) ;
2023-02-10 03:26:08 +01:00
2023-09-13 18:15:01 +02:00
this . $displayName = this . $el . querySelector ( '.display-name' ) ;
2023-02-10 03:26:08 +01:00
this . _receiveTextQueue = [ ] ;
2024-04-17 20:38:33 +02:00
this . _hideTimeout = null ;
2022-12-30 20:34:54 +01:00
}
2024-01-03 16:53:09 +01:00
selectionEmpty ( ) {
return ! window . getSelection ( ) . toString ( )
}
2022-12-30 20:34:54 +01:00
async _onKeyDown ( e ) {
2023-11-24 16:25:30 +01:00
if ( ! this . isShown ( ) ) return
2024-01-03 16:53:09 +01:00
if ( e . code === "KeyC" && ( e . ctrlKey || e . metaKey ) && this . selectionEmpty ( ) ) {
2023-11-24 16:25:30 +01:00
await this . _onCopy ( )
}
else if ( e . code === "Escape" ) {
this . hide ( ) ;
2022-12-30 20:34:54 +01:00
}
2018-09-21 16:05:03 +02:00
}
2023-02-10 03:26:08 +01:00
_onText ( text , peerId ) {
window . blop . play ( ) ;
this . _receiveTextQueue . push ( { text : text , peerId : peerId } ) ;
2023-03-02 15:30:25 +01:00
this . _setDocumentTitleMessages ( ) ;
2024-02-01 00:37:17 +01:00
changeFavicon ( "images/favicon-96x96-notification.png" ) ;
2024-04-17 20:38:33 +02:00
if ( this . isShown ( ) || this . _hideTimeout ) return ;
2024-02-01 00:37:17 +01:00
2023-02-10 03:26:08 +01:00
this . _dequeueRequests ( ) ;
}
_dequeueRequests ( ) {
2024-02-01 00:37:17 +01:00
this . _setDocumentTitleMessages ( ) ;
changeFavicon ( "images/favicon-96x96-notification.png" ) ;
2023-02-10 03:26:08 +01:00
let { text , peerId } = this . _receiveTextQueue . shift ( ) ;
this . _showReceiveTextDialog ( text , peerId ) ;
}
_showReceiveTextDialog ( text , peerId ) {
2023-09-13 18:15:01 +02:00
this . $displayName . innerText = $ ( peerId ) . ui . _displayName ( ) ;
2025-02-13 10:24:27 +01:00
this . $displayName . classList . remove ( "badge-room-ip" , "badge-room-secret" , "badge-room-public-id" ) ;
2023-09-13 18:15:01 +02:00
this . $displayName . classList . add ( $ ( peerId ) . ui . _badgeClassName ( ) ) ;
2023-02-10 03:26:08 +01:00
2023-03-03 12:28:50 +01:00
this . $text . innerText = text ;
2024-01-03 13:54:13 +01:00
2024-02-01 00:37:17 +01:00
// Beautify text if text is not too long
if ( this . $text . innerText . length <= 300000 ) {
// Hacky workaround to replace URLs with link nodes in all cases
// 1. Use text variable, find all valid URLs via regex and replace URLs with placeholder
// 2. Use html variable, find placeholders with regex and replace them with link nodes
let $textShadow = document . createElement ( 'div' ) ;
$textShadow . innerText = text ;
let linkNodes = { } ;
let searchHTML = $textShadow . innerHTML ;
const p = "@" ;
const pRgx = new RegExp ( ` ${ p } \\ d+ ` , 'g' ) ;
let occP = searchHTML . match ( pRgx ) || [ ] ;
let m = 0 ;
const allowedDomainChars = "a-zA-Z0-9áàäčçđéèêŋńñóòôöšŧüžæøåëìíîïðùúýþćěłřśţźǎǐǒǔǥǧǩǯəʒâûœÿãõāēīōūăąĉċďĕėęĝğġģĥħĩĭįı ĵķĸĺļľņňŏőŕŗŝşťũŭůűųŵŷżאבגדהו זחט י ךכלםמן נס עףפץצקרשתװױײ" ;
const urlRgx = new RegExp ( ` (^| \\ n| \\ s|[">< \\ -_~: \\ /?# \\ [ \\ ]@! $ &'()*+,;=%.])(((https?: \\ / \\ /)?(?:[ ${ allowedDomainChars } ](?:[ ${ allowedDomainChars } -]{0,61}[ ${ allowedDomainChars } ])? \\ .)+[ ${ allowedDomainChars } ][ ${ allowedDomainChars } -]{0,61}[ ${ allowedDomainChars } ])(:? \\ d*) \\ /?([ ${ allowedDomainChars } _ \\ / \\ -#.]*)( \\ ?([ ${ allowedDomainChars } \\ -_~: \\ /?# \\ [ \\ ]@! $ &'()*+,;=%.]*))?) ` , 'g' ) ;
$textShadow . innerText = text . replace ( urlRgx ,
( match , whitespaceOrSpecial , url , g3 , scheme ) => {
let link = url ;
// prefix www.example.com with http protocol to prevent it from being a relative link
if ( ! scheme && link . startsWith ( 'www' ) ) {
link = "http://" + link
}
if ( isUrlValid ( link ) ) {
// link is valid -> replace with link node placeholder
// find linkNodePlaceholder that is not yet present in text node
m ++ ;
while ( occP . includes ( ` ${ p } ${ m } ` ) ) {
m ++ ;
2024-01-12 01:23:14 +01:00
}
2024-02-01 00:37:17 +01:00
let linkNodePlaceholder = ` ${ p } ${ m } ` ;
// add linkNodePlaceholder to text node and save a reference to linkNodes object
2024-08-31 00:55:18 +02:00
linkNodes [ linkNodePlaceholder ] = ` <a href=" ${ link } " target="_blank" rel="noreferrer"> ${ url } </a> ` ;
2024-02-01 00:37:17 +01:00
return ` ${ whitespaceOrSpecial } ${ linkNodePlaceholder } ` ;
}
// link is not valid -> do not replace
return match ;
2024-01-03 13:54:13 +01:00
} ) ;
2023-03-03 12:28:50 +01:00
2023-11-20 05:23:21 +01:00
2024-02-01 00:37:17 +01:00
this . $text . innerHTML = $textShadow . innerHTML . replace ( pRgx ,
( m ) => {
let urlNode = linkNodes [ m ] ;
return urlNode ? urlNode : m ;
} ) ;
}
2023-03-02 15:30:25 +01:00
2024-02-01 00:37:17 +01:00
this . _evaluateOverflowing ( this . $text ) ;
2018-09-21 16:05:03 +02:00
this . show ( ) ;
}
2023-03-02 15:30:25 +01:00
_setDocumentTitleMessages ( ) {
2024-02-01 00:37:17 +01:00
document . title = this . _receiveTextQueue . length <= 1
2023-07-06 21:29:36 +02:00
? ` ${ Localization . getTranslation ( "document-titles.message-received" ) } - PairDrop `
: ` ${ Localization . getTranslation ( "document-titles.message-received-plural" , null , { count : this . _receiveTextQueue . length + 1 } ) } - PairDrop ` ;
2023-03-02 15:30:25 +01:00
}
2020-12-20 04:41:16 +01:00
async _onCopy ( ) {
2023-05-27 01:13:49 +02:00
const sanitizedText = this . $text . innerText . replace ( /\u00A0/gm , ' ' ) ;
2023-11-02 02:56:01 +01:00
navigator . clipboard
. writeText ( sanitizedText )
2023-09-13 17:50:46 +02:00
. then ( _ => {
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.copied-to-clipboard" ) ) ;
this . hide ( ) ;
} )
. catch ( _ => {
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.copied-to-clipboard-error" ) ) ;
} ) ;
2023-02-10 03:26:08 +01:00
}
hide ( ) {
super . hide ( ) ;
2024-04-17 20:38:33 +02:00
// If queue is empty -> clear text field | else -> open next message
this . _hideTimeout = setTimeout ( ( ) => {
if ( ! this . _receiveTextQueue . length ) {
this . $text . innerHTML = "" ;
}
else {
this . _dequeueRequests ( ) ;
}
this . _hideTimeout = null ;
2024-01-03 16:52:26 +01:00
} , 500 ) ;
2018-09-21 16:05:03 +02:00
}
}
2023-11-24 16:25:30 +01:00
class ShareTextDialog extends Dialog {
constructor ( ) {
super ( 'share-text-dialog' ) ;
this . $text = this . $el . querySelector ( '.textarea' ) ;
this . $approveMsgBtn = this . $el . querySelector ( 'button[type="submit"]' ) ;
this . $checkbox = this . $el . querySelector ( 'input[type="checkbox"]' )
this . $approveMsgBtn . addEventListener ( 'click' , _ => this . _approveShareText ( ) ) ;
// Only show this per default if user sets checkmark
2023-11-24 17:32:40 +01:00
this . $checkbox . checked = localStorage . getItem ( 'approve-share-text' )
2023-11-24 16:25:30 +01:00
? ShareTextDialog . isApproveShareTextSet ( )
: false ;
this . _setCheckboxValueToLocalStorage ( ) ;
this . $checkbox . addEventListener ( 'change' , _ => this . _setCheckboxValueToLocalStorage ( ) ) ;
Events . on ( 'share-text-dialog' , e => this . _onShareText ( e . detail ) ) ;
Events . on ( 'keydown' , e => this . _onKeyDown ( e ) ) ;
this . $text . addEventListener ( 'input' , _ => this . _evaluateEmptyText ( ) ) ;
}
static isApproveShareTextSet ( ) {
2023-11-24 17:32:40 +01:00
return localStorage . getItem ( 'approve-share-text' ) === "true" ;
2023-11-24 16:25:30 +01:00
}
_setCheckboxValueToLocalStorage ( ) {
2023-11-24 17:32:40 +01:00
localStorage . setItem ( 'approve-share-text' , this . $checkbox . checked ? "true" : "false" ) ;
2023-11-24 16:25:30 +01:00
}
_onKeyDown ( e ) {
if ( ! this . isShown ( ) ) return ;
if ( e . code === "Escape" ) {
this . _approveShareText ( ) ;
}
else if ( e . code === "Enter" && ( e . ctrlKey || e . metaKey ) ) {
if ( this . _textEmpty ( ) ) return ;
this . _approveShareText ( ) ;
}
}
_textEmpty ( ) {
return ! this . $text . innerText || this . $text . innerText === "\n" ;
}
_evaluateEmptyText ( ) {
if ( this . _textEmpty ( ) ) {
this . $approveMsgBtn . setAttribute ( 'disabled' , true ) ;
// remove remaining whitespace on Firefox on text deletion
this . $text . innerText = "" ;
}
else {
this . $approveMsgBtn . removeAttribute ( 'disabled' ) ;
}
this . _evaluateOverflowing ( this . $text ) ;
}
_onShareText ( text ) {
this . $text . innerText = text ;
this . _evaluateEmptyText ( ) ;
this . show ( ) ;
}
_approveShareText ( ) {
Events . fire ( 'activate-share-mode' , { text : this . $text . innerText } ) ;
this . hide ( ) ;
}
hide ( ) {
super . hide ( ) ;
setTimeout ( ( ) => this . $text . innerText = "" , 500 ) ;
}
}
2023-12-05 18:53:21 +01:00
class Base64Dialog extends Dialog {
2023-01-19 04:40:28 +01:00
constructor ( ) {
2023-03-01 10:44:57 +01:00
super ( 'base64-paste-dialog' ) ;
2023-02-20 17:42:02 +01:00
2023-12-05 18:57:31 +01:00
this . $title = this . $el . querySelector ( '.dialog-title' ) ;
2023-03-01 10:44:57 +01:00
this . $pasteBtn = this . $el . querySelector ( '#base64-paste-btn' ) ;
2023-03-06 02:18:07 +01:00
this . $fallbackTextarea = this . $el . querySelector ( '.textarea' ) ;
2023-12-05 18:57:31 +01:00
}
2023-01-25 09:43:32 +01:00
2023-12-05 18:57:31 +01:00
async evaluateBase64Text ( base64Text , hash ) {
this . $title . innerText = Localization . getTranslation ( 'dialogs.base64-title-text' ) ;
if ( base64Text === 'paste' ) {
// ?base64text=paste
// base64 encoded string is ready to be pasted from clipboard
this . preparePasting ( 'text' ) ;
2023-02-20 17:42:02 +01:00
this . show ( ) ;
2023-11-02 02:56:01 +01:00
}
2023-12-05 18:57:31 +01:00
else if ( base64Text === 'hash' ) {
// ?base64text=hash#BASE64ENCODED
// base64 encoded text is url hash which cannot be seen by the server and is faster (recommended)
2023-01-19 14:48:43 +01:00
this . show ( ) ;
2023-12-12 14:19:41 +01:00
await this . processBase64Text ( hash ) ;
2023-12-05 18:57:31 +01:00
}
else {
// ?base64text=BASE64ENCODED
// base64 encoded text is part of the url param. Seen by server and slow (not recommended)
this . show ( ) ;
2023-12-12 14:19:41 +01:00
await this . processBase64Text ( base64Text ) ;
2023-12-05 18:57:31 +01:00
}
}
async evaluateBase64Zip ( base64Zip , hash ) {
this . $title . innerText = Localization . getTranslation ( 'dialogs.base64-title-files' ) ;
if ( base64Zip === 'paste' ) {
// ?base64zip=paste || ?base64zip=true
this . preparePasting ( 'files' ) ;
this . show ( ) ;
}
else if ( base64Zip === 'hash' ) {
// ?base64zip=hash#BASE64ENCODED
// base64 encoded zip file is url hash which cannot be seen by the server
2023-12-12 14:19:41 +01:00
await this . processBase64Zip ( hash ) ;
2023-02-20 17:42:02 +01:00
}
}
_setPasteBtnToProcessing ( ) {
2023-03-03 12:01:43 +01:00
this . $pasteBtn . style . pointerEvents = "none" ;
2023-07-06 21:29:36 +02:00
this . $pasteBtn . innerText = Localization . getTranslation ( "dialogs.base64-processing" ) ;
2023-02-20 17:42:02 +01:00
}
2023-03-06 02:18:07 +01:00
preparePasting ( type ) {
2023-09-18 21:29:29 +02:00
const translateType = type === 'text'
? Localization . getTranslation ( "dialogs.base64-text" )
: Localization . getTranslation ( "dialogs.base64-files" ) ;
2023-03-06 02:18:07 +01:00
if ( navigator . clipboard . readText ) {
2023-09-18 21:29:29 +02:00
this . $pasteBtn . innerText = Localization . getTranslation ( "dialogs.base64-tap-to-paste" , null , { type : translateType } ) ;
2023-03-06 12:20:30 +01:00
this . _clickCallback = _ => this . processClipboard ( type ) ;
this . $pasteBtn . addEventListener ( 'click' , _ => this . _clickCallback ( ) ) ;
2023-11-02 02:56:01 +01:00
}
else {
2023-03-06 02:18:07 +01:00
console . log ( "`navigator.clipboard.readText()` is not available on your browser.\nOn Firefox you can set `dom.events.asyncClipboard.readText` to true under `about:config` for convenience." )
2023-11-08 20:36:46 +01:00
this . $pasteBtn . setAttribute ( 'hidden' , true ) ;
2023-09-18 21:29:29 +02:00
this . $fallbackTextarea . setAttribute ( 'placeholder' , Localization . getTranslation ( "dialogs.base64-paste-to-send" , null , { type : translateType } ) ) ;
2023-03-06 02:18:07 +01:00
this . $fallbackTextarea . removeAttribute ( 'hidden' ) ;
2023-03-06 12:20:30 +01:00
this . _inputCallback = _ => this . processInput ( type ) ;
this . $fallbackTextarea . addEventListener ( 'input' , _ => this . _inputCallback ( ) ) ;
2023-03-06 02:18:07 +01:00
this . $fallbackTextarea . focus ( ) ;
2023-02-20 17:42:02 +01:00
}
2023-03-06 02:18:07 +01:00
}
2023-02-20 17:42:02 +01:00
2023-03-06 02:18:07 +01:00
async processInput ( type ) {
const base64 = this . $fallbackTextarea . textContent ;
this . $fallbackTextarea . textContent = '' ;
2023-12-05 18:57:31 +01:00
await this . processPastedBase64 ( type , base64 ) ;
2023-03-06 02:18:07 +01:00
}
2023-02-20 17:42:02 +01:00
2023-03-06 02:18:07 +01:00
async processClipboard ( type ) {
2023-02-20 17:42:02 +01:00
const base64 = await navigator . clipboard . readText ( ) ;
2023-12-05 18:57:31 +01:00
await this . processPastedBase64 ( type , base64 ) ;
2023-03-06 02:18:07 +01:00
}
2023-02-20 17:42:02 +01:00
2023-12-05 18:57:31 +01:00
async processPastedBase64 ( type , base64 ) {
2023-03-06 02:18:07 +01:00
try {
2023-09-18 21:29:29 +02:00
if ( type === 'text' ) {
2023-03-06 02:18:07 +01:00
await this . processBase64Text ( base64 ) ;
2023-11-02 02:56:01 +01:00
}
else {
2023-03-06 02:18:07 +01:00
await this . processBase64Zip ( base64 ) ;
}
2023-12-05 18:57:31 +01:00
}
catch ( e ) {
2023-07-06 21:29:36 +02:00
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.clipboard-content-incorrect" ) ) ;
2023-03-06 02:18:07 +01:00
console . log ( "Clipboard content is incorrect." )
2023-01-19 14:48:43 +01:00
}
2023-03-06 02:18:07 +01:00
this . hide ( ) ;
2023-01-19 04:40:28 +01:00
}
2023-12-05 18:57:31 +01:00
async processBase64Text ( base64 ) {
this . _setPasteBtnToProcessing ( ) ;
try {
const decodedText = await decodeBase64Text ( base64 ) ;
2023-11-24 16:25:30 +01:00
if ( ShareTextDialog . isApproveShareTextSet ( ) ) {
Events . fire ( 'share-text-dialog' , decodedText ) ;
2023-12-05 18:57:31 +01:00
}
else {
2023-11-24 16:25:30 +01:00
Events . fire ( 'activate-share-mode' , { text : decodedText } ) ;
}
2023-12-05 18:57:31 +01:00
}
catch ( e ) {
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.text-content-incorrect" ) ) ;
console . log ( "Text content incorrect." ) ;
}
this . hide ( ) ;
2023-01-22 16:12:00 +01:00
}
2023-12-05 18:57:31 +01:00
async processBase64Zip ( base64 ) {
2023-02-20 17:42:02 +01:00
this . _setPasteBtnToProcessing ( ) ;
2023-01-19 04:40:28 +01:00
2023-12-05 18:57:31 +01:00
try {
const decodedFiles = await decodeBase64Files ( base64 ) ;
Events . fire ( 'activate-share-mode' , { files : decodedFiles } ) ;
}
catch ( e ) {
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.file-content-incorrect" ) ) ;
console . log ( "File content incorrect." ) ;
2023-01-19 04:40:28 +01:00
}
2023-01-25 09:43:32 +01:00
2023-12-05 18:57:31 +01:00
this . hide ( ) ;
2023-01-25 09:43:32 +01:00
}
2023-02-20 17:42:02 +01:00
hide ( ) {
2023-03-06 12:20:30 +01:00
this . $pasteBtn . removeEventListener ( 'click' , _ => this . _clickCallback ( ) ) ;
this . $fallbackTextarea . removeEventListener ( 'input' , _ => this . _inputCallback ( ) ) ;
2023-12-12 14:19:41 +01:00
this . $fallbackTextarea . setAttribute ( 'disabled' , true ) ;
this . $fallbackTextarea . blur ( ) ;
2023-02-20 17:42:02 +01:00
super . hide ( ) ;
}
2023-01-19 04:40:28 +01:00
}
2023-12-13 17:40:48 +01:00
class AboutUI {
constructor ( ) {
this . $donationBtn = $ ( 'donation-btn' ) ;
this . $twitterBtn = $ ( 'twitter-btn' ) ;
this . $mastodonBtn = $ ( 'mastodon-btn' ) ;
this . $blueskyBtn = $ ( 'bluesky-btn' ) ;
this . $customBtn = $ ( 'custom-btn' ) ;
this . $privacypolicyBtn = $ ( 'privacypolicy-btn' ) ;
Events . on ( 'config' , e => this . _onConfig ( e . detail . buttons ) ) ;
}
async _onConfig ( btnConfig ) {
await this . _evaluateBtnConfig ( this . $donationBtn , btnConfig . donation _button ) ;
await this . _evaluateBtnConfig ( this . $twitterBtn , btnConfig . twitter _button ) ;
await this . _evaluateBtnConfig ( this . $mastodonBtn , btnConfig . mastodon _button ) ;
await this . _evaluateBtnConfig ( this . $blueskyBtn , btnConfig . bluesky _button ) ;
await this . _evaluateBtnConfig ( this . $customBtn , btnConfig . custom _button ) ;
await this . _evaluateBtnConfig ( this . $privacypolicyBtn , btnConfig . privacypolicy _button ) ;
}
async _evaluateBtnConfig ( $btn , config ) {
// if config is not set leave everything as default
if ( ! Object . keys ( config ) . length ) return ;
if ( config . active === "false" ) {
$btn . setAttribute ( 'hidden' , true ) ;
} else {
if ( config . link ) {
$btn . setAttribute ( 'href' , config . link ) ;
}
if ( config . title ) {
$btn . setAttribute ( 'title' , config . title ) ;
// prevent overwriting of custom title when setting different language
$btn . removeAttribute ( 'data-i18n-key' ) ;
$btn . removeAttribute ( 'data-i18n-attrs' ) ;
}
if ( config . icon ) {
$btn . setAttribute ( 'title' , config . title ) ;
// prevent overwriting of custom title when setting different language
$btn . removeAttribute ( 'data-i18n-key' ) ;
$btn . removeAttribute ( 'data-i18n-attrs' ) ;
}
$btn . removeAttribute ( 'hidden' ) ;
}
}
}
2018-09-21 16:05:03 +02:00
class Toast extends Dialog {
constructor ( ) {
super ( 'toast' ) ;
2023-11-24 16:25:30 +01:00
this . $closeBtn = this . $el . querySelector ( '.icon-button' ) ;
this . $text = this . $el . querySelector ( 'span' ) ;
this . $closeBtn . addEventListener ( 'click' , _ => this . hide ( ) ) ;
2023-01-23 04:51:22 +01:00
Events . on ( 'notify-user' , e => this . _onNotify ( e . detail ) ) ;
2023-11-24 16:25:30 +01:00
Events . on ( 'share-mode-changed' , _ => this . hide ( ) ) ;
2018-09-21 16:05:03 +02:00
}
2023-01-23 04:51:22 +01:00
_onNotify ( message ) {
if ( this . hideTimeout ) clearTimeout ( this . hideTimeout ) ;
2023-11-24 16:25:30 +01:00
this . $text . innerText = typeof message === "object" ? message . message : message ;
2018-09-21 16:05:03 +02:00
this . show ( ) ;
2023-10-12 03:39:37 +02:00
if ( typeof message === "object" && message . persistent ) return ;
this . hideTimeout = setTimeout ( ( ) => this . hide ( ) , 5000 ) ;
2018-09-21 16:05:03 +02:00
}
2023-11-24 16:25:30 +01:00
hide ( ) {
if ( this . hideTimeout ) clearTimeout ( this . hideTimeout ) ;
super . hide ( ) ;
}
2018-09-21 16:05:03 +02:00
}
class Notifications {
constructor ( ) {
// Check if the browser supports notifications
2023-10-12 19:02:04 +02:00
if ( ! ( 'Notification' in window ) ) return ;
2018-09-21 22:31:46 +02:00
2023-11-09 03:48:17 +01:00
this . $headerNotificationButton = $ ( 'notification' ) ;
2023-11-24 16:25:30 +01:00
this . $downloadBtn = $ ( 'download-btn' ) ;
2023-11-09 03:48:17 +01:00
this . $headerNotificationButton . addEventListener ( 'click' , _ => this . _requestPermission ( ) ) ;
2023-10-06 02:57:46 +02:00
2023-10-11 23:22:10 +02:00
2023-02-10 03:26:08 +01:00
Events . on ( 'text-received' , e => this . _messageNotification ( e . detail . text , e . detail . peerId ) ) ;
2023-01-21 18:21:58 +01:00
Events . on ( 'files-received' , e => this . _downloadNotification ( e . detail . files ) ) ;
2023-03-06 15:05:13 +01:00
Events . on ( 'files-transfer-request' , e => this . _requestNotification ( e . detail . request , e . detail . peerId ) ) ;
2018-09-21 16:05:03 +02:00
}
2023-11-02 01:31:31 +01:00
async _requestPermission ( ) {
await Notification .
requestPermission ( permission => {
if ( permission !== 'granted' ) {
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.notifications-permissions-error" ) ) ;
return ;
}
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.notifications-enabled" ) ) ;
2023-11-08 20:36:46 +01:00
this . $headerNotificationButton . setAttribute ( 'hidden' , true ) ;
2023-11-02 01:31:31 +01:00
} ) ;
2018-09-21 16:05:03 +02:00
}
2023-02-10 03:26:08 +01:00
_notify ( title , body ) {
2018-09-21 18:54:52 +02:00
const config = {
2018-09-21 16:05:03 +02:00
body : body ,
2018-09-21 18:54:52 +02:00
icon : '/images/logo_transparent_128x128.png' ,
}
2019-03-13 00:08:37 +01:00
let notification ;
2018-09-21 23:19:54 +02:00
try {
2023-02-10 03:26:08 +01:00
notification = new Notification ( title , config ) ;
2018-09-21 23:19:54 +02:00
} catch ( e ) {
2019-03-13 00:08:37 +01:00
// Android doesn't support "new Notification" if service worker is installed
2018-09-21 23:19:54 +02:00
if ( ! serviceWorker || ! serviceWorker . showNotification ) return ;
2023-02-10 03:26:08 +01:00
notification = serviceWorker . showNotification ( title , config ) ;
2018-09-21 18:54:52 +02:00
}
2018-09-21 23:19:54 +02:00
2019-03-13 00:08:37 +01:00
// Notification is persistent on Android. We have to close it manually
2022-12-22 22:41:26 +01:00
const visibilitychangeHandler = ( ) => {
if ( document . visibilityState === 'visible' ) {
2022-09-13 01:35:05 -04:00
notification . close ( ) ;
2022-09-13 08:34:44 -04:00
Events . off ( 'visibilitychange' , visibilitychangeHandler ) ;
2022-12-22 22:41:26 +01:00
}
} ;
2022-09-13 08:34:44 -04:00
Events . on ( 'visibilitychange' , visibilitychangeHandler ) ;
2019-03-13 00:08:37 +01:00
return notification ;
2018-09-21 16:05:03 +02:00
}
2023-02-10 03:26:08 +01:00
_messageNotification ( message , peerId ) {
2022-09-13 01:35:05 -04:00
if ( document . visibilityState !== 'visible' ) {
2023-02-10 03:26:08 +01:00
const peerDisplayName = $ ( peerId ) . ui . _displayName ( ) ;
2023-03-03 12:28:50 +01:00
if ( /^((https?:\/\/|www)[abcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)$/ . test ( message . toLowerCase ( ) ) ) {
2023-07-06 21:29:36 +02:00
const notification = this . _notify ( Localization . getTranslation ( "notifications.link-received" , null , { name : peerDisplayName } ) , message ) ;
2023-11-02 01:31:31 +01:00
this . _bind ( notification , _ => window . open ( message , '_blank' , "noreferrer" ) ) ;
2023-11-02 02:56:01 +01:00
}
else {
2023-07-06 21:29:36 +02:00
const notification = this . _notify ( Localization . getTranslation ( "notifications.message-received" , null , { name : peerDisplayName } ) , message ) ;
2023-01-10 05:07:57 +01:00
this . _bind ( notification , _ => this . _copyText ( message , notification ) ) ;
2022-09-13 01:35:05 -04:00
}
2018-09-21 16:05:03 +02:00
}
}
2023-01-21 18:21:58 +01:00
_downloadNotification ( files ) {
2022-09-13 01:35:05 -04:00
if ( document . visibilityState !== 'visible' ) {
2023-12-15 21:19:56 +01:00
let imagesOnly = files . every ( file => file . type . split ( '/' ) [ 0 ] === 'image' ) ;
2023-07-06 21:29:36 +02:00
let title ;
2023-12-15 21:19:56 +01:00
2023-07-06 21:29:36 +02:00
if ( files . length === 1 ) {
title = ` ${ files [ 0 ] . name } ` ;
2023-11-02 02:56:01 +01:00
}
else {
2023-07-06 21:29:36 +02:00
let fileOther ;
if ( files . length === 2 ) {
fileOther = imagesOnly
? Localization . getTranslation ( "dialogs.file-other-description-image" )
: Localization . getTranslation ( "dialogs.file-other-description-file" ) ;
2023-11-02 02:56:01 +01:00
}
else {
2023-07-06 21:29:36 +02:00
fileOther = imagesOnly
? Localization . getTranslation ( "dialogs.file-other-description-image-plural" , null , { count : files . length - 1 } )
: Localization . getTranslation ( "dialogs.file-other-description-file-plural" , null , { count : files . length - 1 } ) ;
}
title = ` ${ files [ 0 ] . name } ${ fileOther } `
2023-01-21 18:21:58 +01:00
}
2023-07-06 21:29:36 +02:00
const notification = this . _notify ( title , Localization . getTranslation ( "notifications.click-to-download" ) ) ;
2023-01-10 05:07:57 +01:00
this . _bind ( notification , _ => this . _download ( notification ) ) ;
2022-09-13 01:35:05 -04:00
}
2018-09-21 22:31:46 +02:00
}
2023-03-06 15:05:13 +01:00
_requestNotification ( request , peerId ) {
if ( document . visibilityState !== 'visible' ) {
2023-12-15 21:19:56 +01:00
let imagesOnly = request . header . every ( header => header . mime . split ( '/' ) [ 0 ] === 'image' ) ;
let displayName = $ ( peerId ) . querySelector ( '.name' ) . textContent ;
2023-07-06 21:29:36 +02:00
2023-03-06 15:05:13 +01:00
let descriptor ;
2023-07-06 21:29:36 +02:00
if ( request . header . length === 1 ) {
descriptor = imagesOnly
? Localization . getTranslation ( "dialogs.title-image" )
: Localization . getTranslation ( "dialogs.title-file" ) ;
2023-11-02 02:56:01 +01:00
}
else {
2023-07-06 21:29:36 +02:00
descriptor = imagesOnly
? Localization . getTranslation ( "dialogs.title-image-plural" )
: Localization . getTranslation ( "dialogs.title-file-plural" ) ;
2023-03-06 15:05:13 +01:00
}
2023-07-06 21:29:36 +02:00
2023-11-02 02:56:01 +01:00
let title = Localization
. getTranslation ( "notifications.request-title" , null , {
name : displayName ,
count : request . header . length ,
descriptor : descriptor . toLowerCase ( )
} ) ;
2023-07-06 21:29:36 +02:00
const notification = this . _notify ( title , Localization . getTranslation ( "notifications.click-to-show" ) ) ;
2023-03-06 15:05:13 +01:00
}
}
2018-09-21 23:19:54 +02:00
_download ( notification ) {
2023-11-24 16:25:30 +01:00
this . $downloadBtn . click ( ) ;
2018-09-21 23:19:54 +02:00
notification . close ( ) ;
2018-09-21 16:05:03 +02:00
}
2023-11-02 01:31:31 +01:00
async _copyText ( message , notification ) {
if ( await navigator . clipboard . writeText ( message ) ) {
2023-01-10 05:07:57 +01:00
notification . close ( ) ;
2023-07-06 21:29:36 +02:00
this . _notify ( Localization . getTranslation ( "notifications.copied-text" ) ) ;
2023-11-02 02:56:01 +01:00
}
else {
2023-07-06 21:29:36 +02:00
this . _notify ( Localization . getTranslation ( "notifications.copied-text-error" ) ) ;
2023-01-10 05:07:57 +01:00
}
2018-09-21 23:19:54 +02:00
}
_bind ( notification , handler ) {
if ( notification . then ) {
2023-11-02 02:56:01 +01:00
notification . then ( _ => {
serviceWorker
. getNotifications ( )
. then ( _ => {
serviceWorker . addEventListener ( 'notificationclick' , handler ) ;
} )
} ) ;
}
else {
2018-09-21 23:19:54 +02:00
notification . onclick = handler ;
}
2018-09-21 22:31:46 +02:00
}
2018-09-21 16:05:03 +02:00
}
2019-03-13 01:21:44 +01:00
class NetworkStatusUI {
2019-03-12 23:37:50 +01:00
constructor ( ) {
2023-01-07 01:45:52 +01:00
Events . on ( 'offline' , _ => this . _showOfflineMessage ( ) ) ;
Events . on ( 'online' , _ => this . _showOnlineMessage ( ) ) ;
2019-03-13 01:21:44 +01:00
if ( ! navigator . onLine ) this . _showOfflineMessage ( ) ;
2019-03-12 23:37:50 +01:00
}
2019-03-13 01:21:44 +01:00
_showOfflineMessage ( ) {
2023-10-12 18:57:26 +02:00
Events . fire ( 'notify-user' , {
message : Localization . getTranslation ( "notifications.offline" ) ,
persistent : true
} ) ;
2019-03-12 23:37:50 +01:00
}
2019-03-13 01:21:44 +01:00
_showOnlineMessage ( ) {
2023-07-06 21:29:36 +02:00
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.online" ) ) ;
2019-03-12 23:37:50 +01:00
}
}
2019-03-13 01:48:53 +01:00
class WebShareTargetUI {
2023-01-18 21:01:29 +01:00
2023-12-05 18:57:31 +01:00
async evaluateShareTarget ( shareTargetType , title , text , url ) {
if ( shareTargetType === "text" ) {
let shareTargetText ;
if ( url ) {
shareTargetText = url ; // we share only the link - no text.
}
else if ( title && text ) {
shareTargetText = title + '\r\n' + text ;
}
else {
shareTargetText = title + text ;
2023-11-02 02:56:01 +01:00
}
2023-03-28 19:07:33 +02:00
2023-12-05 18:57:31 +01:00
if ( ShareTextDialog . isApproveShareTextSet ( ) ) {
Events . fire ( 'share-text-dialog' , shareTargetText ) ;
}
else {
Events . fire ( 'activate-share-mode' , { text : shareTargetText } ) ;
}
}
else if ( shareTargetType === "files" ) {
let openRequest = window . indexedDB . open ( 'pairdrop_store' )
openRequest . onsuccess = e => {
const db = e . target . result ;
const tx = db . transaction ( 'share_target_files' , 'readwrite' ) ;
const store = tx . objectStore ( 'share_target_files' ) ;
const request = store . getAll ( ) ;
request . onsuccess = _ => {
const fileObjects = request . result ;
let filesReceived = [ ] ;
for ( let i = 0 ; i < fileObjects . length ; i ++ ) {
filesReceived . push ( new File ( [ fileObjects [ i ] . buffer ] , fileObjects [ i ] . name ) ) ;
2023-01-19 01:28:35 +01:00
}
2023-12-05 18:57:31 +01:00
const clearRequest = store . clear ( )
clearRequest . onsuccess = _ => db . close ( ) ;
Events . fire ( 'activate-share-mode' , { files : filesReceived } )
2023-03-09 17:03:44 +01:00
}
2023-01-18 21:01:29 +01:00
}
}
2019-03-13 01:48:53 +01:00
}
}
2019-03-12 23:37:50 +01:00
2023-11-23 19:57:41 +01:00
// Keep for legacy reasons even though this is removed from new PWA installations
2023-01-18 15:45:53 +01:00
class WebFileHandlersUI {
2023-12-05 18:57:31 +01:00
async evaluateLaunchQueue ( ) {
if ( ! "launchQueue" in window ) return ;
2023-01-18 15:45:53 +01:00
2023-12-05 18:57:31 +01:00
launchQueue . setConsumer ( async launchParams => {
console . log ( "Launched with: " , launchParams ) ;
if ( ! launchParams . files . length ) return ;
let files = [ ] ;
for ( let i = 0 ; i < launchParams . files . length ; i ++ ) {
if ( i !== 0 && await launchParams . files [ i ] . isSameEntry ( launchParams . files [ i - 1 ] ) ) continue ;
const file = await launchParams . files [ i ] . getFile ( ) ;
files . push ( file ) ;
}
Events . fire ( 'activate-share-mode' , { files : files } )
} ) ;
2023-01-18 15:45:53 +01:00
}
}
2023-01-17 14:19:51 +01:00
class NoSleepUI {
constructor ( ) {
NoSleepUI . _nosleep = new NoSleep ( ) ;
}
static enable ( ) {
if ( ! this . _interval ) {
NoSleepUI . _nosleep . enable ( ) ;
2023-11-02 01:22:41 +01:00
NoSleepUI . _interval = setInterval ( ( ) => NoSleepUI . disable ( ) , 10000 ) ;
2023-01-17 14:19:51 +01:00
}
}
static disable ( ) {
if ( $$ ( 'x-peer[status]' ) === null ) {
clearInterval ( NoSleepUI . _interval ) ;
NoSleepUI . _nosleep . disable ( ) ;
}
}
}