2018-09-21 16:05:03 +02:00
const $ = query => document . getElementById ( query ) ;
const $$ = query => document . body . querySelector ( query ) ;
2018-09-21 23:19:54 +02:00
window . isProductionEnvironment = ! window . location . host . startsWith ( 'localhost' ) ;
2018-10-11 00:08:07 +02:00
window . iOS = /iPad|iPhone|iPod/ . test ( navigator . userAgent ) && ! window . MSStream ;
2023-01-17 10:41:50 +01:00
window . android = /android/i . test ( navigator . userAgent ) ;
2022-11-09 17:43:27 +01:00
window . pasteMode = { } ;
window . pasteMode . activated = false ;
2018-09-21 16:05:03 +02:00
2020-07-12 19:23:07 +02:00
// set display name
2020-12-16 04:16:53 +01:00
Events . on ( 'display-name' , e => {
2020-12-19 21:05:48 +01:00
const me = e . detail . message ;
2023-03-01 21:35:00 +01:00
const $displayName = $ ( 'display-name' ) ;
$displayName . setAttribute ( 'placeholder' , me . displayName ) ;
2020-07-12 19:23:07 +02:00
} ) ;
2018-09-21 16:05:03 +02:00
class PeersUI {
constructor ( ) {
Events . on ( 'peer-joined' , e => this . _onPeerJoined ( e . detail ) ) ;
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 ) ) ;
2020-03-09 22:40:57 +01:00
Events . on ( 'paste' , e => this . _onPaste ( e ) ) ;
2023-09-13 18:15:01 +02:00
Events . on ( 'room-type-removed' , e => this . _onRoomTypeRemoved ( e . detail . peerId , e . detail . roomType ) ) ;
2023-01-18 15:28:57 +01:00
Events . on ( 'activate-paste-mode' , e => this . _activatePasteMode ( e . detail . files , e . detail . text ) ) ;
2022-12-22 22:41:26 +01:00
this . peers = { } ;
2023-01-17 10:41:50 +01:00
2023-03-03 12:01:43 +01:00
this . $cancelPasteModeBtn = $ ( 'cancel-paste-mode' ) ;
2023-01-18 15:28:57 +01:00
this . $cancelPasteModeBtn . addEventListener ( 'click' , _ => this . _cancelPasteMode ( ) ) ;
2023-01-17 10:41:50 +01:00
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 ( ) ) ;
Events . on ( 'drop' , e => this . _onDrop ( e ) ) ;
2023-01-17 10:41:50 +01:00
Events . on ( 'keydown' , e => this . _onKeyDown ( e ) ) ;
2023-01-22 16:14:27 +01:00
2023-03-01 10:04:37 +01:00
this . $xPeers = $$ ( 'x-peers' ) ;
2023-01-22 16:14:27 +01:00
this . $xNoPeers = $$ ( 'x-no-peers' ) ;
this . $xInstructions = $$ ( 'x-instructions' ) ;
2023-03-01 10:04:37 +01:00
Events . on ( 'peer-added' , _ => this . evaluateOverflowing ( ) ) ;
Events . on ( 'bg-resize' , _ => this . evaluateOverflowing ( ) ) ;
2023-03-01 21:35:00 +01:00
this . $displayName = $ ( 'display-name' ) ;
2023-08-30 14:57:40 +02:00
this . $displayName . setAttribute ( "placeholder" , this . $displayName . dataset . placeholder ) ;
2023-03-01 21:35:00 +01:00
this . $displayName . addEventListener ( 'keydown' , e => this . _onKeyDownDisplayName ( e ) ) ;
this . $displayName . addEventListener ( 'keyup' , e => this . _onKeyUpDisplayName ( e ) ) ;
this . $displayName . addEventListener ( 'blur' , e => this . _saveDisplayName ( e . target . innerText ) ) ;
Events . on ( 'self-display-name-changed' , e => this . _insertDisplayName ( e . detail ) ) ;
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-03-02 15:06:22 +01:00
// Load saved display name on page load
this . _getSavedDisplayName ( ) . then ( displayName => {
2023-03-01 21:35:00 +01:00
console . log ( "Retrieved edited display name:" , displayName )
if ( displayName ) Events . fire ( 'self-display-name-changed' , displayName ) ;
} ) ;
2023-04-20 21:11:08 +02:00
/* prevent animation on load */
setTimeout ( _ => {
this . $xNoPeers . style . animationIterationCount = "1" ;
} , 300 ) ;
2023-03-01 21:35:00 +01:00
}
_insertDisplayName ( displayName ) {
this . $displayName . textContent = displayName ;
}
_onKeyDownDisplayName ( e ) {
if ( e . key === "Enter" || e . key === "Escape" ) {
e . preventDefault ( ) ;
e . target . blur ( ) ;
}
}
_onKeyUpDisplayName ( e ) {
2023-03-02 15:06:22 +01:00
// fix for Firefox inserting a linebreak into div on edit which prevents the placeholder from showing automatically when it is empty
if ( /^(\n|\r|\r\n)$/ . test ( e . target . innerText ) ) e . target . innerText = '' ;
2023-03-01 21:35:00 +01:00
}
async _saveDisplayName ( newDisplayName ) {
2023-03-02 15:06:22 +01:00
newDisplayName = newDisplayName . replace ( /(\n|\r|\r\n)/ , '' )
const savedDisplayName = await this . _getSavedDisplayName ( ) ;
2023-03-01 21:35:00 +01:00
if ( newDisplayName === savedDisplayName ) return ;
if ( newDisplayName ) {
2023-05-04 17:38:51 +02:00
PersistentStorage . set ( 'editedDisplayName' , newDisplayName )
. then ( _ => {
2023-07-06 21:29:36 +02:00
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.display-name-changed-permanently" ) ) ;
2023-05-04 17:38:51 +02:00
} )
. catch ( _ => {
console . log ( "This browser does not support IndexedDB. Use localStorage instead." ) ;
localStorage . setItem ( 'editedDisplayName' , newDisplayName ) ;
2023-07-06 21:29:36 +02:00
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.display-name-changed-temporarily" ) ) ;
2023-05-04 17:38:51 +02:00
} )
. finally ( _ => {
Events . fire ( 'self-display-name-changed' , newDisplayName ) ;
Events . fire ( 'broadcast-send' , { type : 'self-display-name-changed' , detail : newDisplayName } ) ;
} ) ;
2023-03-01 21:35:00 +01:00
} else {
2023-05-04 17:38:51 +02:00
PersistentStorage . delete ( 'editedDisplayName' )
. catch ( _ => {
console . log ( "This browser does not support IndexedDB. Use localStorage instead." )
localStorage . removeItem ( 'editedDisplayName' ) ;
} )
. finally ( _ => {
2023-07-06 21:29:36 +02:00
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.display-name-random-again" ) ) ;
2023-05-04 17:38:51 +02:00
Events . fire ( 'self-display-name-changed' , '' ) ;
Events . fire ( 'broadcast-send' , { type : 'self-display-name-changed' , detail : '' } ) ;
} ) ;
2023-03-01 21:35:00 +01:00
}
}
2023-03-02 15:06:22 +01:00
_getSavedDisplayName ( ) {
return new Promise ( ( resolve ) => {
PersistentStorage . get ( 'editedDisplayName' )
2023-03-13 14:21:26 +01:00
. then ( displayName => {
if ( ! displayName ) displayName = "" ;
resolve ( displayName ) ;
} )
. catch ( _ => {
let displayName = localStorage . getItem ( 'editedDisplayName' ) ;
if ( ! displayName ) displayName = "" ;
resolve ( displayName ) ;
} )
2023-03-02 15:06:22 +01:00
} ) ;
}
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
}
_onKeyDown ( e ) {
2023-01-18 15:28:57 +01:00
if ( document . querySelectorAll ( 'x-dialog[show]' ) . length === 0 && window . pasteMode . activated && e . code === "Escape" ) {
2023-01-17 10:41:50 +01:00
Events . fire ( 'deactivate-paste-mode' ) ;
}
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-09-13 18:15:01 +02:00
peer . _isSameBrowser = _ => BrowserTabsConnector . peerIsSameBrowser ( peer . id ) ;
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 ] ;
new PeerUI ( peer , connectionHash ) ;
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
}
evaluateOverflowing ( ) {
if ( this . $xPeers . clientHeight < this . $xPeers . scrollHeight ) {
this . $xPeers . classList . add ( 'overflowing' ) ;
} else {
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-03-01 10:04:37 +01:00
this . evaluateOverflowing ( ) ;
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 ) {
e . preventDefault ( ) ;
2023-01-22 17:33:19 +01:00
if ( ! $$ ( 'x-peer' ) || ! $$ ( 'x-peer' ) . contains ( e . target ) ) {
2023-01-22 16:14:27 +01:00
this . _activatePasteMode ( e . dataTransfer . files , '' )
}
this . _onDragEnd ( ) ;
}
2023-01-22 17:33:19 +01:00
_onDragOver ( e ) {
e . preventDefault ( ) ;
2023-01-22 16:14:27 +01:00
this . $xInstructions . setAttribute ( 'drop-bg' , 1 ) ;
2023-01-22 17:33:19 +01:00
this . $xNoPeers . setAttribute ( 'drop-bg' , 1 ) ;
2023-01-22 16:14:27 +01:00
}
_onDragEnd ( ) {
this . $xInstructions . removeAttribute ( 'drop-bg' , 1 ) ;
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-01-07 03:04:48 +01:00
if ( document . querySelectorAll ( 'x-dialog[show]' ) . length === 0 ) {
2022-11-09 17:43:27 +01:00
// prevent send on paste when dialog is open
e . preventDefault ( )
const files = e . clipboardData . files ;
const text = e . clipboardData . getData ( "Text" ) ;
2023-01-18 15:28:57 +01:00
if ( files . length === 0 && text . length === 0 ) return ;
2022-11-09 17:43:27 +01:00
this . _activatePasteMode ( files , text ) ;
}
}
_activatePasteMode ( files , text ) {
2023-01-18 15:28:57 +01:00
if ( ! window . pasteMode . activated && ( files . length > 0 || text . length > 0 ) ) {
2022-11-09 17:43:27 +01:00
let descriptor ;
let noPeersMessage ;
2023-07-06 21:29:36 +02:00
const openPairDrop = Localization . getTranslation ( "dialogs.activate-paste-mode-base" ) ;
const andOtherFiles = Localization . getTranslation ( "dialogs.activate-paste-mode-and-other-files" , null , { count : files . length - 1 } ) ;
const sharedText = Localization . getTranslation ( "dialogs.activate-paste-mode-shared-text" ) ;
2022-11-09 17:43:27 +01:00
if ( files . length === 1 ) {
2023-07-06 21:29:36 +02:00
noPeersMessage = ` ${ openPairDrop } <br><i> ${ files [ 0 ] . name } </i> ` ;
2022-11-09 17:43:27 +01:00
} else if ( files . length > 1 ) {
2023-07-06 21:29:36 +02:00
noPeersMessage = ` ${ openPairDrop } <br><i> ${ files [ 0 ] . name } </i> ${ andOtherFiles } ` ;
2023-01-18 15:28:57 +01:00
} else {
2023-07-06 21:29:36 +02:00
noPeersMessage = ` ${ openPairDrop } <br> ${ sharedText } ` ;
2022-11-09 17:43:27 +01:00
}
2023-07-06 21:29:36 +02:00
this . $xInstructions . querySelector ( 'p' ) . innerHTML = noPeersMessage ;
2023-01-22 16:14:27 +01:00
this . $xInstructions . querySelector ( 'p' ) . style . display = 'block' ;
2023-07-06 21:29:36 +02:00
this . $xInstructions . setAttribute ( 'desktop' , Localization . getTranslation ( "instructions.click-to-send" ) ) ;
this . $xInstructions . setAttribute ( 'mobile' , Localization . getTranslation ( "instructions.tap-to-send" ) ) ;
2022-11-09 17:43:27 +01:00
2023-01-22 16:14:27 +01:00
this . $xNoPeers . querySelector ( 'h2' ) . innerHTML = noPeersMessage ;
2022-11-09 17:43:27 +01:00
const _callback = ( e ) => this . _sendClipboardData ( e , files , text ) ;
Events . on ( 'paste-pointerdown' , _callback ) ;
2023-03-06 11:59:56 +01:00
Events . on ( 'deactivate-paste-mode' , _ => this . _deactivatePasteMode ( _callback ) , { once : true } ) ;
2022-11-09 17:43:27 +01:00
2023-01-17 10:41:50 +01:00
this . $cancelPasteModeBtn . removeAttribute ( 'hidden' ) ;
2022-11-09 17:43:27 +01:00
window . pasteMode . descriptor = descriptor ;
window . pasteMode . activated = true ;
2023-01-18 15:28:57 +01:00
console . log ( 'Paste mode activated.' ) ;
Events . fire ( 'paste-mode-changed' ) ;
2022-11-09 17:43:27 +01:00
}
}
_cancelPasteMode ( ) {
2023-01-17 10:41:50 +01:00
Events . fire ( 'deactivate-paste-mode' ) ;
2022-11-09 17:43:27 +01:00
}
2023-01-17 10:41:50 +01:00
_deactivatePasteMode ( _callback ) {
if ( window . pasteMode . activated ) {
2022-11-09 17:43:27 +01:00
window . pasteMode . descriptor = undefined ;
window . pasteMode . activated = false ;
Events . off ( 'paste-pointerdown' , _callback ) ;
2023-01-22 16:14:27 +01:00
this . $xInstructions . querySelector ( 'p' ) . innerText = '' ;
this . $xInstructions . querySelector ( 'p' ) . style . display = 'none' ;
2023-01-18 15:28:57 +01:00
2023-07-06 21:29:36 +02:00
this . $xInstructions . setAttribute ( 'desktop' , Localization . getTranslation ( "instructions.x-instructions" , "desktop" ) ) ;
this . $xInstructions . setAttribute ( 'mobile' , Localization . getTranslation ( "instructions.x-instructions" , "mobile" ) ) ;
2023-01-18 15:28:57 +01:00
2023-07-06 21:29:36 +02:00
this . $xNoPeers . querySelector ( 'h2' ) . innerHTML = Localization . getTranslation ( "instructions.no-peers-title" ) ;
2023-01-18 15:28:57 +01:00
2023-01-19 06:43:24 +01:00
this . $cancelPasteModeBtn . setAttribute ( 'hidden' , "" ) ;
2022-11-09 17:43:27 +01:00
2023-01-18 15:28:57 +01:00
console . log ( 'Paste mode deactivated.' )
Events . fire ( 'paste-mode-changed' ) ;
2022-11-09 17:43:27 +01:00
}
}
_sendClipboardData ( e , files , text ) {
// send the pasted file/text content
const peerId = e . detail . peerId ;
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
} ) ;
} else if ( text . length > 0 ) {
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-03-01 10:04:37 +01:00
constructor ( peer , connectionHash ) {
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-03-01 10:04:37 +01:00
this . _initDom ( ) ;
this . _bindListeners ( ) ;
$$ ( 'x-peers' ) . appendChild ( this . $el )
Events . fire ( 'peer-added' ) ;
this . $xInstructions = $$ ( 'x-instructions' ) ;
}
2018-09-21 16:05:03 +02:00
html ( ) {
2022-11-09 17:43:27 +01:00
let title ;
2023-01-18 15:28:57 +01:00
let input = '' ;
2022-11-09 17:43:27 +01:00
if ( window . pasteMode . activated ) {
2023-07-06 21:29:36 +02:00
title = Localization . getTranslation ( "peer-ui.click-to-send-paste-mode" , null , { descriptor : window . pasteMode . descriptor } ) ;
2022-11-09 17:43:27 +01:00
} else {
2023-07-06 21:29:36 +02:00
title = Localization . getTranslation ( "peer-ui.click-to-send" ) ;
2023-01-14 01:43:44 +01:00
input = '<input type="file" multiple>' ;
2022-11-09 17:43:27 +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-01-14 01:43:44 +01:00
$ { input }
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 >
2023-07-06 21:29:36 +02:00
< span class = "connection-hash font-body2" title = "${ Localization.getTranslation(" peer - ui . connection - hash ") }" > < / s p a n >
2023-03-01 10:04:37 +01:00
< / 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-03-06 15:39:24 +01:00
this . $el . querySelector ( '.connection-hash' ) . textContent = this . _connectionHash ;
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 } ` ) ) ;
}
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 ( ) ;
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 )
2023-03-01 10:04:37 +01:00
this . _callbackTouchStart = e => this . _onTouchStart ( e )
2023-01-18 15:28:57 +01:00
this . _callbackTouchEnd = e => this . _onTouchEnd ( e )
this . _callbackPointerDown = e => this . _onPointerDown ( e )
2023-09-13 18:15:01 +02:00
2023-01-22 17:33:19 +01:00
// PasteMode
2023-01-18 15:28:57 +01:00
Events . on ( 'paste-mode-changed' , _ => this . _onPasteModeChanged ( ) ) ;
}
_onPasteModeChanged ( ) {
this . html ( ) ;
this . _bindListeners ( ) ;
}
_bindListeners ( ) {
2022-11-09 17:43:27 +01:00
if ( ! window . pasteMode . activated ) {
2023-01-18 15:28:57 +01:00
// Remove Events Paste Mode
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 ) ;
2022-11-09 17:43:27 +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 ) ;
// Add Events Paste Mode
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 ( ) ;
Events . fire ( 'paste-pointerdown' , {
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 ;
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' ) ;
2018-09-21 16:05:03 +02: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-01-17 10:41:50 +01:00
} else {
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 ) {
e . preventDefault ( ) ;
Events . fire ( 'files-selected' , {
2023-01-22 16:14:27 +01:00
files : e . dataTransfer . files ,
2018-09-21 16:05:03 +02:00
to : this . _peer . id
} ) ;
this . _onDragEnd ( ) ;
}
_onDragOver ( ) {
this . $el . setAttribute ( 'drop' , 1 ) ;
2023-01-22 16:14:27 +01:00
this . $xInstructions . setAttribute ( 'drop-peer' , 1 ) ;
2018-09-21 16:05:03 +02:00
}
_onDragEnd ( ) {
this . $el . removeAttribute ( 'drop' ) ;
2023-01-22 16:14:27 +01:00
this . $xInstructions . removeAttribute ( 'drop-peer' , 1 ) ;
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-03-01 10:04:37 +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-03-01 10:04:37 +01:00
} else if ( this . _touchTimer ) { // this was a long tap
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-02-10 03:26:08 +01:00
this . $el . querySelectorAll ( '[close]' ) . forEach ( el => el . addEventListener ( 'click' , _ => this . hide ( ) ) ) ;
2018-09-21 16:05:03 +02:00
this . $autoFocus = this . $el . querySelector ( '[autofocus]' ) ;
2023-02-08 12:55:28 +01:00
Events . on ( 'peer-disconnected' , e => this . _onPeerDisconnected ( e . detail ) ) ;
2023-09-13 18:15:01 +02:00
this . $discoveryWrapper = $$ ( 'footer .discovery-wrapper' ) ;
2018-09-21 16:05:03 +02:00
}
show ( ) {
this . $el . setAttribute ( 'show' , 1 ) ;
if ( this . $autoFocus ) this . $autoFocus . focus ( ) ;
}
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-01-10 05:07:57 +01:00
if ( this . $autoFocus ) {
document . activeElement . blur ( ) ;
window . blur ( ) ;
}
2023-01-20 01:36:15 +01:00
document . title = 'PairDrop' ;
2023-01-20 15:56:20 +01:00
document . 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-09-13 18:15:01 +02:00
evaluateFooterBadges ( ) {
if ( this . $discoveryWrapper . querySelectorAll ( 'div:last-of-type > span[hidden]' ) . length < 2 ) {
this . $discoveryWrapper . classList . remove ( 'row' ) ;
this . $discoveryWrapper . classList . add ( 'column' ) ;
} else {
this . $discoveryWrapper . classList . remove ( 'column' ) ;
this . $discoveryWrapper . classList . add ( 'row' ) ;
}
Events . fire ( 'bg-resize' ) ;
}
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 ( ) ) ;
this . $languageButtons = this . $el . querySelectorAll ( ".language-buttons button" ) ;
this . $languageButtons . forEach ( $btn => {
$btn . addEventListener ( "click" , e => this . selectLanguage ( e ) ) ;
} )
Events . on ( 'keydown' , e => this . _onKeyDown ( e ) ) ;
}
_onKeyDown ( e ) {
if ( this . isShown ( ) && e . code === "Escape" ) {
this . hide ( ) ;
}
}
show ( ) {
if ( Localization . isSystemLocale ( ) ) {
this . $languageButtons [ 0 ] . focus ( ) ;
} else {
let locale = Localization . getLocale ( ) ;
for ( let i = 0 ; i < this . $languageButtons . length ; i ++ ) {
const $btn = this . $languageButtons [ i ] ;
if ( $btn . value === locale ) {
$btn . focus ( ) ;
break ;
}
}
}
super . show ( ) ;
}
selectLanguage ( e ) {
e . preventDefault ( )
let languageCode = e . target . value ;
if ( languageCode ) {
localStorage . setItem ( 'language-code' , languageCode ) ;
} else {
localStorage . removeItem ( 'language-code' ) ;
}
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' ;
} else if ( bytes >= 1048576 ) {
return Math . round ( bytes / 1048576 ) + ' MB' ;
} else if ( bytes > 1024 ) {
2023-01-25 09:59:38 +01:00
return Math . round ( bytes / 1024 ) + ' KB' ;
2023-01-17 10:41:50 +01:00
} else {
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" ) ;
} else if ( files . length >= 2 ) {
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-09-13 18:15:01 +02:00
_onFilesReceived ( peerId , files , imagesOnly , totalSize ) {
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-03-03 12:01:43 +01:00
this . _nextFiles ( ) ;
2023-09-13 18:15:01 +02:00
2023-01-17 10:41:50 +01:00
window . blop . play ( ) ;
}
2023-03-03 12:01:43 +01:00
_nextFiles ( ) {
2018-09-21 16:05:03 +02:00
if ( this . _busy ) return ;
this . _busy = true ;
2023-09-13 18:15:01 +02:00
const { peerId , displayName , connectionHash , files , imagesOnly , totalSize , badgeClassName } = this . _filesQueue . shift ( ) ;
this . _displayFiles ( peerId , displayName , connectionHash , files , imagesOnly , totalSize , badgeClassName ) ;
2018-09-21 16:05:03 +02:00
}
_dequeueFile ( ) {
if ( ! this . _filesQueue . length ) { // nothing to do
this . _busy = false ;
return ;
}
// dequeue next file
setTimeout ( _ => {
this . _busy = false ;
2023-01-17 10:41:50 +01:00
this . _nextFiles ( ) ;
2018-09-21 16:05:03 +02:00
} , 300 ) ;
}
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 ) ;
} else {
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-01-17 10:41:50 +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-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 ( ) ;
} else {
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-03-03 12:01:43 +01:00
this . $downloadBtn . style . pointerEvents = "none" ;
setTimeout ( _ => this . $downloadBtn . style . pointerEvents = "unset" , 2000 ) ;
} ;
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-03-13 00:04:48 +01:00
document . 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-03-13 00:04:48 +01:00
setTimeout ( _ => {
2023-03-03 12:01:43 +01:00
if ( canShare ) {
this . $shareBtn . click ( ) ;
} else {
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' ) ;
} else {
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 ( ) {
2023-03-03 12:01:43 +01:00
this . $shareBtn . setAttribute ( 'hidden' , '' ) ;
2023-01-17 10:41:50 +01:00
this . $previewBox . innerHTML = '' ;
super . hide ( ) ;
this . _dequeueFile ( ) ;
}
}
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-05-09 03:21:10 +02:00
if ( this . isShown ( ) && 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-03-03 12:01:43 +01:00
this . $receiveTitle . innerText = ` ${ request . imagesOnly ? 'Image' : 'File' } Transfer Request `
2023-07-06 21:29:36 +02:00
document . title = ` ${ Localization . getTranslation ( "document-titles.file-transfer-requested" ) } - PairDrop ` ;
2023-01-20 15:56:20 +01:00
document . changeFavicon ( "images/favicon-96x96-notification.png" ) ;
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
setTimeout ( _ => this . $previewBox . innerHTML = '' , 300 ) ;
2018-09-21 16:05:03 +02:00
super . hide ( ) ;
2023-05-04 17:38:51 +02:00
// show next request
2023-02-10 03:26:08 +01:00
setTimeout ( _ => this . _dequeueRequests ( ) , 500 ) ;
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 ( ) {
this . $inputKeyChars . forEach ( char => char . removeAttribute ( "disabled" ) ) ;
}
_disableChars ( ) {
this . $inputKeyChars . forEach ( char => char . setAttribute ( "disabled" , "" ) ) ;
}
_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 ( ) ;
} else if ( e . key === "ArrowRight" && nextSibling ) {
e . preventDefault ( ) ;
nextSibling . focus ( ) ;
} else if ( e . key === "ArrowLeft" && previousSibling ) {
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-01-10 05:07:57 +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/ ,
( ) => this . $pairSubmitBtn . removeAttribute ( "disabled" ) ,
( ) => this . $pairSubmitBtn . setAttribute ( "disabled" , "" ) ,
( ) => 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-connected' , _ => this . _onWsConnected ( ) ) ;
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 ) ) ;
this . evaluateUrlAttributes ( ) ;
this . pairPeer = { } ;
}
_onKeyDown ( e ) {
if ( this . isShown ( ) && e . code === "Escape" ) {
// Timeout to prevent paste mode from getting cancelled simultaneously
setTimeout ( _ => this . _close ( ) , 50 ) ;
}
}
_onPaste ( e ) {
e . preventDefault ( ) ;
let pastedKey = e . clipboardData . getData ( "Text" ) . replace ( /\D/g , '' ) . substring ( 0 , 6 ) ;
this . inputKeyContainer . _onPaste ( pastedKey ) ;
}
2023-01-10 05:07:57 +01:00
evaluateUrlAttributes ( ) {
const urlParams = new URLSearchParams ( window . location . search ) ;
2023-09-13 18:15:01 +02:00
if ( urlParams . has ( 'pair_key' ) ) {
this . _pairDeviceJoin ( urlParams . get ( 'pair_key' ) ) ;
2023-05-30 01:21:17 +02:00
const url = getUrlWithoutArguments ( ) ;
2023-09-13 18:15:01 +02:00
window . history . replaceState ( { } , "Rewrite URL" , url ) ; //remove pair_key from url
2023-01-10 05:07:57 +01:00
}
}
_onWsConnected ( ) {
2023-05-11 21:04:10 +02:00
this . _evaluateNumberRoomSecrets ( ) ;
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-09-13 18:15:01 +02:00
this . $key . innerText = ` ${ this . pairKey . substring ( 0 , 3 ) } ${ this . pairKey . substring ( 3 , 6 ) } `
2023-01-10 05:07:57 +01:00
// Display the QR code for the url
const qr = new QRCode ( {
2023-09-13 18:15:01 +02:00
content : this . _getPairURL ( ) ,
2023-02-24 16:08:36 +01:00
width : 150 ,
height : 150 ,
2023-01-10 05:07:57 +01:00
padding : 0 ,
background : "transparent" ,
2023-03-01 10:04:37 +01:00
color : ` rgb(var(--text-color)) ` ,
2023-01-10 05:07:57 +01:00
ecl : "L" ,
join : true
} ) ;
this . $qrCode . innerHTML = qr . svg ( ) ;
2023-09-13 18:15:01 +02:00
this . inputKeyContainer . _enableChars ( ) ;
2023-01-10 05:07:57 +01:00
this . show ( ) ;
}
2023-09-13 18:15:01 +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-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 ;
}
PersistentStorage . addRoomSecret ( roomSecret , displayName , deviceName )
. 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 ( ) ;
} )
. finally ( _ => {
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 ) {
PersistentStorage . deleteRoomSecret ( roomSecret ) . then ( _ => {
this . _evaluateNumberRoomSecrets ( ) ;
2023-05-04 17:38:51 +02:00
} ) ;
2023-01-10 05:07:57 +01:00
}
_evaluateNumberRoomSecrets ( ) {
PersistentStorage . getAllRoomSecrets ( ) . then ( roomSecrets => {
if ( roomSecrets . length > 0 ) {
2023-09-13 18:15:01 +02:00
this . $editPairedDevicesHeaderBtn . removeAttribute ( 'hidden' ) ;
2023-02-08 04:06:15 +01:00
this . $footerInstructionsPairedDevices . removeAttribute ( 'hidden' ) ;
2023-01-10 05:07:57 +01:00
} else {
2023-09-13 18:15:01 +02:00
this . $editPairedDevicesHeaderBtn . setAttribute ( 'hidden' , '' ) ;
2023-02-08 04:06:15 +01:00
this . $footerInstructionsPairedDevices . setAttribute ( 'hidden' , '' ) ;
2023-01-10 05:07:57 +01:00
}
2023-09-13 18:15:01 +02:00
super . evaluateFooterBadges ( ) ;
2023-05-04 17:38:51 +02:00
} ) ;
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-09-13 18:15:01 +02:00
this . $footerInstructionsPairedDevices = $$ ( '.discovery-wrapper .badge-room-secret' ) ;
2023-05-04 17:38:51 +02:00
$ ( 'edit-paired-devices' ) . addEventListener ( 'click' , _ => this . _onEditPairedDevices ( ) ) ;
2023-09-13 18:15:01 +02:00
this . $footerInstructionsPairedDevices . 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-05-09 03:21:10 +02:00
if ( this . isShown ( ) && 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-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-05-04 17:38:51 +02:00
roomSecretsEntries . forEach ( roomSecretsEntry => {
let $pairedDevice = document . createElement ( 'div' ) ;
$pairedDevice . classList = [ "paired-device" ] ;
$pairedDevice . innerHTML = `
< div class = "display-name" >
< span > $ { roomSecretsEntry . display _name } < / s p a n >
< / d i v >
< div class = "device-name" >
< span > $ { roomSecretsEntry . device _name } < / s p a n >
< / d i v >
< div class = "button-wrapper" >
2023-09-14 18:15:07 +02:00
< label class = "auto-accept pointer" > $ { autoAcceptString }
2023-05-04 17:38:51 +02:00
< input type = "checkbox" $ { roomSecretsEntry . auto _accept ? "checked" : "" } >
< / l a b e l >
2023-09-14 18:15:07 +02:00
< button class = "button" type = "button" > $ { unpairString } < / b u t t o n >
2023-05-04 17:38:51 +02:00
< / d i v > `
$pairedDevice . querySelector ( 'input[type="checkbox"]' ) . addEventListener ( 'click' , e => {
PersistentStorage . updateRoomSecretAutoAccept ( roomSecretsEntry . secret , e . target . checked ) . then ( roomSecretsEntry => {
Events . fire ( 'auto-accept-updated' , {
'roomSecret' : roomSecretsEntry . entry . secret ,
'autoAccept' : e . target . checked
} ) ;
} ) ;
} ) ;
$pairedDevice . querySelector ( 'button' ) . addEventListener ( 'click' , e => {
PersistentStorage . deleteRoomSecret ( roomSecretsEntry . secret ) . then ( roomSecret => {
Events . fire ( 'room-secrets-deleted' , [ roomSecret ] ) ;
Events . fire ( 'evaluate-number-room-secrets' ) ;
e . target . parentNode . parentNode . remove ( ) ;
} ) ;
} )
2023-09-13 18:15:01 +02:00
this . $pairedDevicesWrapper . html = "" ;
2023-05-04 17:38:51 +02:00
this . $pairedDevicesWrapper . appendChild ( $pairedDevice )
} )
}
hide ( ) {
super . hide ( ) ;
setTimeout ( _ => {
this . $pairedDevicesWrapper . innerHTML = ""
} , 300 ) ;
}
_onEditPairedDevices ( ) {
this . _initDOM ( ) . then ( _ => this . show ( ) ) ;
2023-03-02 16:30:47 +01:00
}
_clearRoomSecrets ( ) {
2023-05-04 17:38:51 +02:00
PersistentStorage . getAllRoomSecrets ( )
. then ( roomSecrets => {
PersistentStorage . clearRoomSecrets ( ) . finally ( _ => {
Events . fire ( 'room-secrets-deleted' , roomSecrets ) ;
Events . fire ( 'evaluate-number-room-secrets' ) ;
2023-07-06 21:29:36 +02:00
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.pairing-cleared" ) ) ;
2023-05-04 17:38:51 +02:00
this . hide ( ) ;
} )
} ) ;
}
_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-09-13 18:15:01 +02:00
PersistentStorage . updateRoomSecretNames ( peer . _roomIds [ "secret" ] , peer . name . displayName , peer . name . deviceName ) . then ( roomSecretEntry => {
2023-05-04 17:38:51 +02:00
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' ) ;
this . $footerInstructionsPublicRoomDevices = $$ ( '.discovery-wrapper .badge-room-public-id' ) ;
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 ( ) ) ;
this . $footerInstructionsPublicRoomDevices . addEventListener ( 'click' , _ => this . _onHeaderBtnClick ( ) ) ;
this . inputKeyContainer = new InputKeyContainer (
this . $el . querySelector ( '.input-key-container' ) ,
/[a-z|A-Z]/ ,
( ) => this . $joinSubmitBtn . removeAttribute ( "disabled" ) ,
( ) => this . $joinSubmitBtn . setAttribute ( "disabled" , "" ) ,
( ) => 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 ) ) ;
this . evaluateUrlAttributes ( ) ;
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 ) {
if ( this . isShown ( ) && e . code === "Escape" ) {
this . hide ( ) ;
}
}
_onPaste ( e ) {
e . preventDefault ( ) ;
let pastedKey = e . clipboardData . getData ( "Text" ) ;
this . inputKeyContainer . _onPaste ( pastedKey ) ;
}
_onHeaderBtnClick ( ) {
if ( this . roomId ) {
this . show ( ) ;
} else {
this . _createPublicRoom ( ) ;
}
}
_createPublicRoom ( ) {
Events . fire ( 'create-public-room' ) ;
}
_onPublicRoomCreated ( roomId ) {
this . roomId = roomId ;
this . setIdAndQrCode ( ) ;
this . show ( ) ;
sessionStorage . setItem ( 'public_room_id' , roomId ) ;
}
setIdAndQrCode ( ) {
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 ( {
content : this . _getShareRoomURL ( ) ,
width : 150 ,
height : 150 ,
padding : 0 ,
background : "transparent" ,
color : ` rgb(var(--text-color)) ` ,
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-09-13 18:15:01 +02:00
this . $footerInstructionsPublicRoomDevices . innerText = Localization . getTranslation ( "footer.public-room-devices" , null , {
roomId : this . roomId . toUpperCase ( )
} ) ;
this . $footerInstructionsPublicRoomDevices . removeAttribute ( 'hidden' ) ;
2023-09-14 20:06:17 +02:00
2023-09-13 18:15:01 +02:00
super . evaluateFooterBadges ( ) ;
}
_getShareRoomURL ( ) {
let url = new URL ( location . href ) ;
url . searchParams . append ( 'room_key' , this . roomId )
return url . href ;
}
evaluateUrlAttributes ( ) {
const urlParams = new URLSearchParams ( window . location . search ) ;
if ( urlParams . has ( 'room_key' ) ) {
this . _joinPublicRoom ( urlParams . get ( 'room_key' ) ) ;
const url = getUrlWithoutArguments ( ) ;
window . history . replaceState ( { } , "Rewrite URL" , url ) ; //remove pair_key from url
}
}
_onWsConnected ( ) {
let roomId = sessionStorage . getItem ( 'public_room_id' ) ;
if ( ! roomId ) return ;
this . roomId = roomId ;
2023-09-14 20:06:17 +02:00
this . setIdAndQrCode ( ) ;
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-09-14 20:06:17 +02:00
this . setIdAndQrCode ( ) ;
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' ) ;
this . $footerInstructionsPublicRoomDevices . setAttribute ( 'hidden' , '' ) ;
super . evaluateFooterBadges ( ) ;
}
}
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-03-01 10:04:37 +01:00
Events . on ( 'text-recipient' , e => this . _onRecipient ( e . detail . peerId , e . detail . deviceName ) ) ;
2023-03-01 10:44:57 +01:00
this . $text = this . $el . querySelector ( '#text-input' ) ;
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-02-10 03:26:08 +01:00
this . $text . addEventListener ( 'input' , e => this . _onChange ( e ) ) ;
2023-09-14 20:06:17 +02:00
Events . on ( 'keydown' , e => this . _onKeyDown ( e ) ) ;
2022-12-30 20:34:54 +01:00
}
async _onKeyDown ( e ) {
2023-05-27 01:13:49 +02:00
if ( ! this . isShown ( ) ) return ;
if ( e . code === "Escape" ) {
this . hide ( ) ;
} else if ( e . code === "Enter" && ( e . ctrlKey || e . metaKey ) ) {
if ( this . _textInputEmpty ( ) ) return ;
this . _send ( ) ;
2022-12-30 20:34:54 +01:00
}
2018-09-21 16:05:03 +02:00
}
2023-02-10 03:26:08 +01:00
_textInputEmpty ( ) {
2023-05-16 02:53:56 +02:00
return ! this . $text . innerText || this . $text . innerText === "\n" ;
2023-02-10 03:26:08 +01:00
}
_onChange ( e ) {
if ( this . _textInputEmpty ( ) ) {
this . $submit . setAttribute ( 'disabled' , '' ) ;
} else {
this . $submit . removeAttribute ( 'disabled' ) ;
}
}
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 ;
2023-09-13 18:15:01 +02:00
this . $peerDisplayName . classList . remove ( "badge-room-ip" , "badge-room-secret" , "badge-room-public-id" ) ;
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-14 01:43:44 +01:00
this . $text . focus ( ) ;
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-01-17 10:47:44 +01:00
text : this . $text . innerText
2018-09-21 16:05:03 +02:00
} ) ;
2023-01-14 01:43:44 +01:00
this . $text . value = "" ;
2023-02-10 03:26:08 +01:00
this . hide ( ) ;
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 = [ ] ;
2022-12-30 20:34:54 +01:00
}
async _onKeyDown ( e ) {
2023-05-09 03:21:10 +02:00
if ( this . isShown ( ) ) {
2023-01-25 10:01:45 +01:00
if ( e . code === "KeyC" && ( e . ctrlKey || e . metaKey ) ) {
await this . _onCopy ( )
this . hide ( ) ;
} 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 ( ) ;
2023-05-09 03:21:10 +02:00
if ( this . isShown ( ) ) return ;
2023-02-10 03:26:08 +01:00
this . _dequeueRequests ( ) ;
}
_dequeueRequests ( ) {
if ( ! this . _receiveTextQueue . length ) return ;
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 ( ) ;
this . $displayName . classList . remove ( "badge-room-ip" , "badge-room-secret" , "badge-room-public-id" ) ;
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 ;
this . $text . classList . remove ( 'text-center' ) ;
// Beautify text if text is short
if ( text . length < 2000 ) {
// replace urls with actual links
this . $text . innerHTML = this . $text . innerHTML . replace ( /((https?:\/\/|www)[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)/g , url => {
return ` <a href=" ${ url } " target="_blank"> ${ url } </a> ` ;
} ) ;
2018-09-21 16:05:03 +02:00
}
2023-03-03 12:28:50 +01:00
2023-03-02 15:30:25 +01:00
this . _setDocumentTitleMessages ( ) ;
2023-01-20 15:56:20 +01:00
document . changeFavicon ( "images/favicon-96x96-notification.png" ) ;
2018-09-21 16:05:03 +02:00
this . show ( ) ;
}
2023-03-02 15:30:25 +01:00
_setDocumentTitleMessages ( ) {
document . title = ! this . _receiveTextQueue . length
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-09-13 17:50:46 +02:00
navigator . clipboard . writeText ( sanitizedText )
. 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 ( ) ;
setTimeout ( _ => this . _dequeueRequests ( ) , 500 ) ;
2018-09-21 16:05:03 +02:00
}
}
2023-01-19 04:40:28 +01:00
class Base64ZipDialog extends Dialog {
constructor ( ) {
2023-03-01 10:44:57 +01:00
super ( 'base64-paste-dialog' ) ;
2023-01-19 04:40:28 +01:00
const urlParams = new URL ( window . location ) . searchParams ;
2023-01-22 16:12:00 +01:00
const base64Text = urlParams . get ( 'base64text' ) ;
2023-02-20 17:42:02 +01:00
const base64Zip = urlParams . get ( 'base64zip' ) ;
const base64Hash = window . location . hash . substring ( 1 ) ;
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-01-25 09:43:32 +01:00
2023-01-22 16:12:00 +01:00
if ( base64Text ) {
2023-02-20 17:42:02 +01:00
this . show ( ) ;
if ( base64Text === "paste" ) {
// ?base64text=paste
// base64 encoded string is ready to be pasted from clipboard
2023-07-06 21:29:36 +02:00
this . preparePasting ( Localization . getTranslation ( "dialogs.base64-text" ) ) ;
2023-02-20 17:42:02 +01:00
} else if ( base64Text === "hash" ) {
// ?base64text=hash#BASE64ENCODED
// base64 encoded string is url hash which is never sent to server and faster (recommended)
this . processBase64Text ( base64Hash )
. catch ( _ => {
2023-07-06 21:29:36 +02:00
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.text-content-incorrect" ) ) ;
2023-03-06 02:18:07 +01:00
console . log ( "Text content incorrect." ) ;
2023-02-20 17:42:02 +01:00
} ) . finally ( _ => {
this . hide ( ) ;
} ) ;
} else {
// ?base64text=BASE64ENCODED
// base64 encoded string was part of url param (not recommended)
this . processBase64Text ( base64Text )
. catch ( _ => {
2023-07-06 21:29:36 +02:00
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.text-content-incorrect" ) ) ;
2023-03-06 02:18:07 +01:00
console . log ( "Text content incorrect." ) ;
2023-02-20 17:42:02 +01:00
} ) . finally ( _ => {
this . hide ( ) ;
} ) ;
2023-01-25 09:43:32 +01:00
}
2023-02-20 17:42:02 +01:00
} else if ( base64Zip ) {
2023-01-19 14:48:43 +01:00
this . show ( ) ;
2023-02-20 17:42:02 +01:00
if ( base64Zip === "hash" ) {
// ?base64zip=hash#BASE64ENCODED
// base64 encoded zip file is url hash which is never sent to the server
this . processBase64Zip ( base64Hash )
. catch ( _ => {
2023-07-06 21:29:36 +02:00
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.file-content-incorrect" ) ) ;
2023-03-06 02:18:07 +01:00
console . log ( "File content incorrect." ) ;
2023-02-20 17:42:02 +01:00
} ) . finally ( _ => {
this . hide ( ) ;
} ) ;
} else {
// ?base64zip=paste || ?base64zip=true
2023-07-06 21:29:36 +02:00
this . preparePasting ( Localization . getTranslation ( "dialogs.base64-files" ) ) ;
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 ) {
if ( navigator . clipboard . readText ) {
2023-07-06 21:29:36 +02:00
this . $pasteBtn . innerText = Localization . getTranslation ( "dialogs.base64-tap-to-paste" , { type : type } ) ;
2023-03-06 12:20:30 +01:00
this . _clickCallback = _ => this . processClipboard ( type ) ;
this . $pasteBtn . addEventListener ( 'click' , _ => this . _clickCallback ( ) ) ;
2023-03-06 02:18:07 +01:00
} else {
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." )
this . $pasteBtn . setAttribute ( 'hidden' , '' ) ;
2023-07-06 21:29:36 +02:00
this . $fallbackTextarea . setAttribute ( 'placeholder' , Localization . getTranslation ( "dialogs.base64-paste-to-send" , { type : type } ) ) ;
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-03-06 12:20:30 +01:00
await this . processBase64 ( 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-03-06 02:18:07 +01:00
await this . processBase64 ( type , base64 ) ;
}
2023-02-20 17:42:02 +01:00
2023-03-06 12:20:30 +01:00
isValidBase64 ( base64 ) {
try {
// check if input is base64 encoded
window . atob ( base64 ) ;
return true ;
} catch ( e ) {
// input is not base64 string.
return false ;
}
}
2023-03-06 02:18:07 +01:00
async processBase64 ( type , base64 ) {
2023-03-06 12:20:30 +01:00
if ( ! base64 || ! this . isValidBase64 ( base64 ) ) return ;
this . _setPasteBtnToProcessing ( ) ;
2023-03-06 02:18:07 +01:00
try {
if ( type === "text" ) {
await this . processBase64Text ( base64 ) ;
} else {
await this . processBase64Zip ( base64 ) ;
}
} catch ( _ ) {
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-01-22 16:12:00 +01:00
processBase64Text ( base64Text ) {
2023-02-20 17:42:02 +01:00
return new Promise ( ( resolve ) => {
this . _setPasteBtnToProcessing ( ) ;
2023-01-25 09:43:32 +01:00
let decodedText = decodeURIComponent ( escape ( window . atob ( base64Text ) ) ) ;
Events . fire ( 'activate-paste-mode' , { files : [ ] , text : decodedText } ) ;
2023-02-20 17:42:02 +01:00
resolve ( ) ;
} ) ;
2023-01-22 16:12:00 +01:00
}
2023-02-20 17:42:02 +01:00
async processBase64Zip ( base64zip ) {
this . _setPasteBtnToProcessing ( ) ;
let bstr = atob ( base64zip ) , n = bstr . length , u8arr = new Uint8Array ( n ) ;
while ( n -- ) {
u8arr [ n ] = bstr . charCodeAt ( n ) ;
}
2023-01-19 04:40:28 +01:00
2023-02-20 17:42:02 +01:00
const zipBlob = new File ( [ u8arr ] , 'archive.zip' ) ;
2023-01-19 04:40:28 +01:00
2023-02-20 17:42:02 +01:00
let files = [ ] ;
const zipEntries = await zipper . getEntries ( zipBlob ) ;
for ( let i = 0 ; i < zipEntries . length ; i ++ ) {
let fileBlob = await zipper . getData ( zipEntries [ i ] ) ;
files . push ( new File ( [ fileBlob ] , zipEntries [ i ] . filename ) ) ;
2023-01-19 04:40:28 +01:00
}
2023-02-20 17:42:02 +01:00
Events . fire ( 'activate-paste-mode' , { files : files , text : "" } ) ;
2023-01-19 04:40:28 +01:00
}
2023-01-25 09:43:32 +01:00
clearBrowserHistory ( ) {
2023-05-30 01:21:17 +02:00
const url = getUrlWithoutArguments ( ) ;
window . history . replaceState ( { } , "Rewrite URL" , url ) ;
2023-01-25 09:43:32 +01:00
}
2023-02-20 17:42:02 +01:00
hide ( ) {
this . clearBrowserHistory ( ) ;
2023-03-06 12:20:30 +01:00
this . $pasteBtn . removeEventListener ( 'click' , _ => this . _clickCallback ( ) ) ;
this . $fallbackTextarea . removeEventListener ( 'input' , _ => this . _inputCallback ( ) ) ;
2023-02-20 17:42:02 +01:00
super . hide ( ) ;
}
2023-01-19 04:40:28 +01:00
}
2018-09-21 16:05:03 +02:00
class Toast extends Dialog {
constructor ( ) {
super ( 'toast' ) ;
2023-01-23 04:51:22 +01:00
Events . on ( 'notify-user' , e => this . _onNotify ( e . detail ) ) ;
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-05-26 20:24:29 +02:00
this . $el . innerText = message ;
2018-09-21 16:05:03 +02:00
this . show ( ) ;
2023-01-27 01:27:22 +01:00
this . hideTimeout = setTimeout ( _ => this . hide ( ) , 5000 ) ;
2018-09-21 16:05:03 +02:00
}
}
class Notifications {
constructor ( ) {
// Check if the browser supports notifications
if ( ! ( 'Notification' in window ) ) return ;
2018-09-21 22:31:46 +02:00
2018-09-21 16:05:03 +02:00
// Check whether notification permissions have already been granted
if ( Notification . permission !== 'granted' ) {
this . $button = $ ( 'notification' ) ;
this . $button . removeAttribute ( 'hidden' ) ;
2023-01-10 05:07:57 +01:00
this . $button . addEventListener ( 'click' , _ => this . _requestPermission ( ) ) ;
2018-09-21 16:05:03 +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
}
_requestPermission ( ) {
Notification . requestPermission ( permission => {
if ( permission !== 'granted' ) {
Events . fire ( 'notify-user' , Notifications . PERMISSION _ERROR || 'Error' ) ;
return ;
}
2023-07-06 21:29:36 +02:00
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.notifications-enabled" ) ) ;
2018-09-21 16:05:03 +02:00
this . $button . setAttribute ( 'hidden' , 1 ) ;
} ) ;
}
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-01-10 05:07:57 +01:00
this . _bind ( notification , _ => window . open ( message , '_blank' , null , true ) ) ;
2022-09-13 01:35:05 -04: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-01-21 18:21:58 +01:00
let imagesOnly = true ;
for ( let i = 0 ; i < files . length ; i ++ ) {
2023-01-23 20:09:35 +01:00
if ( files [ i ] . type . split ( '/' ) [ 0 ] !== 'image' ) {
2023-01-21 18:21:58 +01:00
imagesOnly = false ;
break ;
}
}
2023-07-06 21:29:36 +02:00
let title ;
if ( files . length === 1 ) {
title = ` ${ files [ 0 ] . name } ` ;
} else {
let fileOther ;
if ( files . length === 2 ) {
fileOther = imagesOnly
? Localization . getTranslation ( "dialogs.file-other-description-image" )
: Localization . getTranslation ( "dialogs.file-other-description-file" ) ;
} else {
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' ) {
let imagesOnly = true ;
for ( let i = 0 ; i < request . header . length ; i ++ ) {
if ( request . header [ i ] . mime . split ( '/' ) [ 0 ] !== 'image' ) {
imagesOnly = false ;
break ;
}
}
2023-07-06 21:29:36 +02:00
let displayName = $ ( peerId ) . querySelector ( '.name' ) . textContent
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-03-06 15:05:13 +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
let title = Localization . getTranslation ( "notifications.request-title" , null , {
name : displayName ,
count : request . header . length ,
descriptor : descriptor . toLowerCase ( )
} ) ;
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-03-06 15:05:13 +01:00
$ ( 'download-btn' ) . click ( ) ;
2018-09-21 23:19:54 +02:00
notification . close ( ) ;
2018-09-21 16:05:03 +02:00
}
2018-09-21 23:19:54 +02:00
_copyText ( message , notification ) {
2023-01-10 05:07:57 +01:00
if ( navigator . clipboard . writeText ( message ) ) {
notification . close ( ) ;
2023-07-06 21:29:36 +02:00
this . _notify ( Localization . getTranslation ( "notifications.copied-text" ) ) ;
2023-01-10 05:07:57 +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-01-23 20:09:35 +01:00
notification . then ( _ => serviceWorker . getNotifications ( ) . then ( _ => {
2018-09-21 23:19:54 +02:00
serviceWorker . addEventListener ( 'notificationclick' , handler ) ;
} ) ) ;
} else {
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-07-06 21:29:36 +02:00
Events . fire ( 'notify-user' , Localization . getTranslation ( "notifications.offline" ) ) ;
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 {
constructor ( ) {
2023-01-18 21:01:29 +01:00
const urlParams = new URL ( window . location ) . searchParams ;
const share _target _type = urlParams . get ( "share-target" )
if ( share _target _type ) {
if ( share _target _type === "text" ) {
const title = urlParams . get ( 'title' ) || '' ;
const text = urlParams . get ( 'text' ) || '' ;
const url = urlParams . get ( 'url' ) || '' ;
let shareTargetText ;
if ( url ) {
2023-03-28 19:07:33 +02:00
shareTargetText = url ; // we share only the link - no text.
2023-01-18 21:01:29 +01:00
} else if ( title && text ) {
shareTargetText = title + '\r\n' + text ;
} else {
shareTargetText = title + text ;
}
Events . fire ( 'activate-paste-mode' , { files : [ ] , text : shareTargetText } )
} else if ( share _target _type === "files" ) {
2023-03-09 17:03:44 +01:00
let openRequest = window . indexedDB . open ( 'pairdrop_store' )
openRequest . onsuccess = e => {
const db = e . target . result ;
2023-01-19 01:28:35 +01:00
const tx = db . transaction ( 'share_target_files' , 'readwrite' ) ;
const store = tx . objectStore ( 'share_target_files' ) ;
const request = store . getAll ( ) ;
request . onsuccess = _ => {
2023-03-09 17:03:44 +01:00
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
const clearRequest = store . clear ( )
clearRequest . onsuccess = _ => db . close ( ) ;
2023-03-28 19:07:33 +02:00
Events . fire ( 'activate-paste-mode' , { files : filesReceived , text : "" } )
2023-01-19 01:28:35 +01:00
}
2023-03-09 17:03:44 +01:00
}
2023-01-18 21:01:29 +01:00
}
2023-05-30 01:21:17 +02:00
const url = getUrlWithoutArguments ( ) ;
window . history . replaceState ( { } , "Rewrite URL" , url ) ;
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-01-18 15:45:53 +01:00
class WebFileHandlersUI {
constructor ( ) {
2023-02-02 15:19:28 +01:00
const urlParams = new URL ( window . location ) . searchParams ;
if ( urlParams . has ( "file_handler" ) && "launchQueue" in window ) {
2023-01-18 15:45:53 +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 fileHandle = launchParams . files [ i ] ;
const file = await fileHandle . getFile ( ) ;
files . push ( file ) ;
}
Events . fire ( 'activate-paste-mode' , { files : files , text : "" } )
launchParams = null ;
} ) ;
2023-05-30 01:21:17 +02:00
const url = getUrlWithoutArguments ( ) ;
window . history . replaceState ( { } , "Rewrite URL" , url ) ;
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 ( ) ;
NoSleepUI . _interval = setInterval ( _ => NoSleepUI . disable ( ) , 10000 ) ;
}
}
static disable ( ) {
if ( $$ ( 'x-peer[status]' ) === null ) {
clearInterval ( NoSleepUI . _interval ) ;
NoSleepUI . _nosleep . disable ( ) ;
}
}
}
2023-01-10 05:07:57 +01:00
class PersistentStorage {
constructor ( ) {
if ( ! ( 'indexedDB' in window ) ) {
2023-01-22 17:34:33 +01:00
PersistentStorage . logBrowserNotCapable ( ) ;
2023-01-10 05:07:57 +01:00
return ;
}
2023-05-04 17:38:51 +02:00
const DBOpenRequest = window . indexedDB . open ( 'pairdrop_store' , 4 ) ;
2023-01-10 05:07:57 +01:00
DBOpenRequest . onerror = ( e ) => {
2023-01-22 17:34:33 +01:00
PersistentStorage . logBrowserNotCapable ( ) ;
2023-01-10 05:07:57 +01:00
console . log ( 'Error initializing database: ' ) ;
2023-01-22 17:34:33 +01:00
console . log ( e )
2023-01-10 05:07:57 +01:00
} ;
DBOpenRequest . onsuccess = ( ) => {
console . log ( 'Database initialised.' ) ;
} ;
DBOpenRequest . onupgradeneeded = ( e ) => {
const db = e . target . result ;
2023-05-04 17:38:51 +02:00
const txn = e . target . transaction ;
2023-01-10 05:07:57 +01:00
db . onerror = e => console . log ( 'Error loading database: ' + e ) ;
2023-05-04 17:38:51 +02:00
console . log ( ` Upgrading IndexedDB database from version ${ e . oldVersion } to version ${ e . newVersion } ` ) ;
if ( e . oldVersion === 0 ) {
// initiate v1
2023-01-19 01:28:35 +01:00
db . createObjectStore ( 'keyval' ) ;
2023-05-04 17:38:51 +02:00
let roomSecretsObjectStore1 = db . createObjectStore ( 'room_secrets' , { autoIncrement : true } ) ;
roomSecretsObjectStore1 . createIndex ( 'secret' , 'secret' , { unique : true } ) ;
2023-01-19 01:28:35 +01:00
}
2023-05-04 17:38:51 +02:00
if ( e . oldVersion <= 1 ) {
// migrate to v2
db . createObjectStore ( 'share_target_files' ) ;
2023-01-19 01:28:35 +01:00
}
2023-05-04 17:38:51 +02:00
if ( e . oldVersion <= 2 ) {
// migrate to v3
db . deleteObjectStore ( 'share_target_files' ) ;
2023-03-09 17:03:44 +01:00
db . createObjectStore ( 'share_target_files' , { autoIncrement : true } ) ;
2023-05-04 17:38:51 +02:00
}
if ( e . oldVersion <= 3 ) {
// migrate to v4
let roomSecretsObjectStore4 = txn . objectStore ( 'room_secrets' ) ;
roomSecretsObjectStore4 . createIndex ( 'display_name' , 'display_name' ) ;
roomSecretsObjectStore4 . createIndex ( 'auto_accept' , 'auto_accept' ) ;
2023-01-19 01:28:35 +01:00
}
2023-01-10 05:07:57 +01:00
}
}
2023-01-22 17:34:33 +01:00
static logBrowserNotCapable ( ) {
2023-01-18 22:42:47 +01:00
console . log ( "This browser does not support IndexedDB. Paired devices will be gone after the browser is closed." ) ;
2023-01-10 05:07:57 +01:00
}
static set ( key , value ) {
return new Promise ( ( resolve , reject ) => {
2023-01-10 17:22:36 +01:00
const DBOpenRequest = window . indexedDB . open ( 'pairdrop_store' ) ;
2023-01-10 05:07:57 +01:00
DBOpenRequest . onsuccess = ( e ) => {
const db = e . target . result ;
const transaction = db . transaction ( 'keyval' , 'readwrite' ) ;
const objectStore = transaction . objectStore ( 'keyval' ) ;
const objectStoreRequest = objectStore . put ( value , key ) ;
objectStoreRequest . onsuccess = _ => {
console . log ( ` Request successful. Added key-pair: ${ key } - ${ value } ` ) ;
2023-01-23 00:03:26 +01:00
resolve ( value ) ;
2023-01-10 05:07:57 +01:00
} ;
}
DBOpenRequest . onerror = ( e ) => {
reject ( e ) ;
}
} )
}
static get ( key ) {
return new Promise ( ( resolve , reject ) => {
2023-01-10 17:22:36 +01:00
const DBOpenRequest = window . indexedDB . open ( 'pairdrop_store' ) ;
2023-01-10 05:07:57 +01:00
DBOpenRequest . onsuccess = ( e ) => {
const db = e . target . result ;
2023-05-04 17:38:51 +02:00
const transaction = db . transaction ( 'keyval' , 'readonly' ) ;
2023-01-10 05:07:57 +01:00
const objectStore = transaction . objectStore ( 'keyval' ) ;
const objectStoreRequest = objectStore . get ( key ) ;
objectStoreRequest . onsuccess = _ => {
console . log ( ` Request successful. Retrieved key-pair: ${ key } - ${ objectStoreRequest . result } ` ) ;
resolve ( objectStoreRequest . result ) ;
}
}
DBOpenRequest . onerror = ( e ) => {
reject ( e ) ;
}
} ) ;
}
2023-01-19 01:28:35 +01:00
2023-01-10 05:07:57 +01:00
static delete ( key ) {
return new Promise ( ( resolve , reject ) => {
2023-01-10 17:22:36 +01:00
const DBOpenRequest = window . indexedDB . open ( 'pairdrop_store' ) ;
2023-01-10 05:07:57 +01:00
DBOpenRequest . onsuccess = ( e ) => {
const db = e . target . result ;
const transaction = db . transaction ( 'keyval' , 'readwrite' ) ;
const objectStore = transaction . objectStore ( 'keyval' ) ;
const objectStoreRequest = objectStore . delete ( key ) ;
objectStoreRequest . onsuccess = _ => {
console . log ( ` Request successful. Deleted key: ${ key } ` ) ;
resolve ( ) ;
} ;
}
DBOpenRequest . onerror = ( e ) => {
reject ( e ) ;
}
} )
}
2023-05-04 17:38:51 +02:00
static addRoomSecret ( roomSecret , displayName , deviceName ) {
2023-01-10 05:07:57 +01:00
return new Promise ( ( resolve , reject ) => {
2023-01-10 17:22:36 +01:00
const DBOpenRequest = window . indexedDB . open ( 'pairdrop_store' ) ;
2023-01-10 05:07:57 +01:00
DBOpenRequest . onsuccess = ( e ) => {
const db = e . target . result ;
const transaction = db . transaction ( 'room_secrets' , 'readwrite' ) ;
const objectStore = transaction . objectStore ( 'room_secrets' ) ;
2023-05-04 17:38:51 +02:00
const objectStoreRequest = objectStore . add ( {
'secret' : roomSecret ,
'display_name' : displayName ,
'device_name' : deviceName ,
'auto_accept' : false
} ) ;
objectStoreRequest . onsuccess = e => {
console . log ( ` Request successful. RoomSecret added: ${ e . target . result } ` ) ;
2023-01-10 05:07:57 +01:00
resolve ( ) ;
}
}
DBOpenRequest . onerror = ( e ) => {
reject ( e ) ;
}
} )
}
2023-05-04 17:38:51 +02:00
static async getAllRoomSecrets ( ) {
2023-05-11 21:04:10 +02:00
try {
const roomSecrets = await this . getAllRoomSecretEntries ( ) ;
let secrets = [ ] ;
for ( let i = 0 ; i < roomSecrets . length ; i ++ ) {
secrets . push ( roomSecrets [ i ] . secret ) ;
}
console . log ( ` Request successful. Retrieved ${ secrets . length } room_secrets ` ) ;
return ( secrets ) ;
} catch ( e ) {
this . logBrowserNotCapable ( ) ;
return false ;
2023-05-04 17:38:51 +02:00
}
}
static getAllRoomSecretEntries ( ) {
2023-01-10 05:07:57 +01:00
return new Promise ( ( resolve , reject ) => {
2023-01-10 17:22:36 +01:00
const DBOpenRequest = window . indexedDB . open ( 'pairdrop_store' ) ;
2023-01-10 05:07:57 +01:00
DBOpenRequest . onsuccess = ( e ) => {
const db = e . target . result ;
2023-05-04 17:38:51 +02:00
const transaction = db . transaction ( 'room_secrets' , 'readonly' ) ;
2023-01-10 05:07:57 +01:00
const objectStore = transaction . objectStore ( 'room_secrets' ) ;
const objectStoreRequest = objectStore . getAll ( ) ;
objectStoreRequest . onsuccess = e => {
2023-05-04 17:38:51 +02:00
resolve ( e . target . result ) ;
2023-01-10 05:07:57 +01:00
}
}
DBOpenRequest . onerror = ( e ) => {
reject ( e ) ;
}
} ) ;
}
2023-05-04 17:38:51 +02:00
static getRoomSecretEntry ( roomSecret ) {
return new Promise ( ( resolve , reject ) => {
const DBOpenRequest = window . indexedDB . open ( 'pairdrop_store' ) ;
DBOpenRequest . onsuccess = ( e ) => {
const db = e . target . result ;
const transaction = db . transaction ( 'room_secrets' , 'readonly' ) ;
const objectStore = transaction . objectStore ( 'room_secrets' ) ;
const objectStoreRequestKey = objectStore . index ( "secret" ) . getKey ( roomSecret ) ;
objectStoreRequestKey . onsuccess = e => {
const key = e . target . result ;
if ( ! key ) {
console . log ( ` Nothing to retrieve. Entry for room_secret not existing: ${ roomSecret } ` ) ;
resolve ( ) ;
return ;
}
const objectStoreRequestRetrieval = objectStore . get ( key ) ;
objectStoreRequestRetrieval . onsuccess = e => {
console . log ( ` Request successful. Retrieved entry for room_secret: ${ key } ` ) ;
resolve ( {
"entry" : e . target . result ,
"key" : key
} ) ;
}
objectStoreRequestRetrieval . onerror = ( e ) => {
reject ( e ) ;
}
} ;
}
DBOpenRequest . onerror = ( e ) => {
reject ( e ) ;
}
} ) ;
}
static deleteRoomSecret ( roomSecret ) {
2023-01-10 05:07:57 +01:00
return new Promise ( ( resolve , reject ) => {
2023-01-10 17:22:36 +01:00
const DBOpenRequest = window . indexedDB . open ( 'pairdrop_store' ) ;
2023-01-10 05:07:57 +01:00
DBOpenRequest . onsuccess = ( e ) => {
const db = e . target . result ;
const transaction = db . transaction ( 'room_secrets' , 'readwrite' ) ;
const objectStore = transaction . objectStore ( 'room_secrets' ) ;
2023-05-04 17:38:51 +02:00
const objectStoreRequestKey = objectStore . index ( "secret" ) . getKey ( roomSecret ) ;
2023-01-10 05:07:57 +01:00
objectStoreRequestKey . onsuccess = e => {
if ( ! e . target . result ) {
2023-05-04 17:38:51 +02:00
console . log ( ` Nothing to delete. room_secret not existing: ${ roomSecret } ` ) ;
2023-01-10 05:07:57 +01:00
resolve ( ) ;
return ;
}
2023-05-04 17:38:51 +02:00
const key = e . target . result ;
const objectStoreRequestDeletion = objectStore . delete ( key ) ;
2023-01-10 05:07:57 +01:00
objectStoreRequestDeletion . onsuccess = _ => {
2023-05-04 17:38:51 +02:00
console . log ( ` Request successful. Deleted room_secret: ${ key } ` ) ;
resolve ( roomSecret ) ;
2023-01-10 05:07:57 +01:00
}
objectStoreRequestDeletion . onerror = ( e ) => {
reject ( e ) ;
}
} ;
}
DBOpenRequest . onerror = ( e ) => {
reject ( e ) ;
}
} )
}
static clearRoomSecrets ( ) {
return new Promise ( ( resolve , reject ) => {
2023-01-10 17:22:36 +01:00
const DBOpenRequest = window . indexedDB . open ( 'pairdrop_store' ) ;
2023-01-10 05:07:57 +01:00
DBOpenRequest . onsuccess = ( e ) => {
const db = e . target . result ;
const transaction = db . transaction ( 'room_secrets' , 'readwrite' ) ;
const objectStore = transaction . objectStore ( 'room_secrets' ) ;
const objectStoreRequest = objectStore . clear ( ) ;
objectStoreRequest . onsuccess = _ => {
console . log ( 'Request successful. All room_secrets cleared' ) ;
resolve ( ) ;
} ;
}
DBOpenRequest . onerror = ( e ) => {
reject ( e ) ;
}
} )
}
2023-05-04 17:38:51 +02:00
static updateRoomSecretNames ( roomSecret , displayName , deviceName ) {
return this . updateRoomSecret ( roomSecret , undefined , displayName , deviceName ) ;
}
static updateRoomSecretAutoAccept ( roomSecret , autoAccept ) {
return this . updateRoomSecret ( roomSecret , undefined , undefined , undefined , autoAccept ) ;
}
static updateRoomSecret ( roomSecret , updatedRoomSecret = undefined , updatedDisplayName = undefined , updatedDeviceName = undefined , updatedAutoAccept = undefined ) {
return new Promise ( ( resolve , reject ) => {
const DBOpenRequest = window . indexedDB . open ( 'pairdrop_store' ) ;
DBOpenRequest . onsuccess = ( e ) => {
const db = e . target . result ;
this . getRoomSecretEntry ( roomSecret )
. then ( roomSecretEntry => {
if ( ! roomSecretEntry ) {
resolve ( false ) ;
return ;
}
const transaction = db . transaction ( 'room_secrets' , 'readwrite' ) ;
const objectStore = transaction . objectStore ( 'room_secrets' ) ;
// Do not use `updatedRoomSecret ?? roomSecretEntry.entry.secret` to ensure compatibility with older browsers
const updatedRoomSecretEntry = {
'secret' : updatedRoomSecret !== undefined ? updatedRoomSecret : roomSecretEntry . entry . secret ,
'display_name' : updatedDisplayName !== undefined ? updatedDisplayName : roomSecretEntry . entry . display _name ,
'device_name' : updatedDeviceName !== undefined ? updatedDeviceName : roomSecretEntry . entry . device _name ,
'auto_accept' : updatedAutoAccept !== undefined ? updatedAutoAccept : roomSecretEntry . entry . auto _accept
} ;
const objectStoreRequestUpdate = objectStore . put ( updatedRoomSecretEntry , roomSecretEntry . key ) ;
objectStoreRequestUpdate . onsuccess = e => {
console . log ( ` Request successful. Updated room_secret: ${ roomSecretEntry . key } ` ) ;
resolve ( {
"entry" : updatedRoomSecretEntry ,
"key" : roomSecretEntry . key
} ) ;
}
objectStoreRequestUpdate . onerror = ( e ) => {
reject ( e ) ;
}
} )
. catch ( e => reject ( e ) ) ;
} ;
DBOpenRequest . onerror = e => reject ( e ) ;
} )
}
2023-01-10 05:07:57 +01:00
}
2019-03-13 01:21:44 +01:00
2023-05-04 17:38:51 +02:00
class BrowserTabsConnector {
2023-03-01 21:35:00 +01:00
constructor ( ) {
this . bc = new BroadcastChannel ( 'pairdrop' ) ;
this . bc . addEventListener ( 'message' , e => this . _onMessage ( e ) ) ;
2023-05-04 17:38:51 +02:00
Events . on ( 'broadcast-send' , e => this . _broadcastSend ( e . detail ) ) ;
2023-03-01 21:35:00 +01:00
}
2023-05-04 17:38:51 +02:00
_broadcastSend ( message ) {
2023-03-01 21:35:00 +01:00
this . bc . postMessage ( message ) ;
}
_onMessage ( e ) {
2023-05-04 17:38:51 +02:00
console . log ( 'Broadcast:' , e . data )
switch ( e . data . type ) {
case 'self-display-name-changed' :
Events . fire ( 'self-display-name-changed' , e . data . detail ) ;
break ;
}
}
static peerIsSameBrowser ( peerId ) {
2023-09-13 18:15:01 +02:00
let peerIdsBrowser = JSON . parse ( localStorage . getItem ( "peer_ids_browser" ) ) ;
2023-05-04 17:38:51 +02:00
return peerIdsBrowser
? peerIdsBrowser . indexOf ( peerId ) !== - 1
: false ;
}
static async addPeerIdToLocalStorage ( ) {
2023-09-13 18:15:01 +02:00
const peerId = sessionStorage . getItem ( "peer_id" ) ;
2023-05-04 17:38:51 +02:00
if ( ! peerId ) return false ;
let peerIdsBrowser = [ ] ;
2023-09-13 18:15:01 +02:00
let peerIdsBrowserOld = JSON . parse ( localStorage . getItem ( "peer_ids_browser" ) ) ;
2023-05-11 21:04:10 +02:00
2023-05-04 17:38:51 +02:00
if ( peerIdsBrowserOld ) peerIdsBrowser . push ( ... peerIdsBrowserOld ) ;
peerIdsBrowser . push ( peerId ) ;
peerIdsBrowser = peerIdsBrowser . filter ( onlyUnique ) ;
2023-09-13 18:15:01 +02:00
localStorage . setItem ( "peer_ids_browser" , JSON . stringify ( peerIdsBrowser ) ) ;
2023-05-11 21:04:10 +02:00
return peerIdsBrowser ;
2023-05-04 17:38:51 +02:00
}
static async removePeerIdFromLocalStorage ( peerId ) {
2023-09-13 18:15:01 +02:00
let peerIdsBrowser = JSON . parse ( localStorage . getItem ( "peer_ids_browser" ) ) ;
2023-05-04 17:38:51 +02:00
const index = peerIdsBrowser . indexOf ( peerId ) ;
peerIdsBrowser . splice ( index , 1 ) ;
2023-09-13 18:15:01 +02:00
localStorage . setItem ( "peer_ids_browser" , JSON . stringify ( peerIdsBrowser ) ) ;
2023-05-04 17:38:51 +02:00
return peerId ;
}
2023-05-11 21:04:10 +02:00
static async removeOtherPeerIdsFromLocalStorage ( ) {
2023-09-13 18:15:01 +02:00
const peerId = sessionStorage . getItem ( "peer_id" ) ;
2023-05-11 21:04:10 +02:00
if ( ! peerId ) return false ;
let peerIdsBrowser = [ peerId ] ;
2023-09-13 18:15:01 +02:00
localStorage . setItem ( "peer_ids_browser" , JSON . stringify ( peerIdsBrowser ) ) ;
2023-05-11 21:04:10 +02:00
return peerIdsBrowser ;
2023-03-01 21:35:00 +01:00
}
}
2023-01-17 10:50:28 +01:00
class PairDrop {
2018-09-21 16:05:03 +02:00
constructor ( ) {
2023-09-14 20:06:17 +02:00
Events . on ( 'initial-translation-loaded' , _ => {
2023-01-10 05:07:57 +01:00
const server = new ServerConnection ( ) ;
const peers = new PeersManager ( server ) ;
const peersUI = new PeersUI ( ) ;
2023-08-30 14:57:40 +02:00
const languageSelectDialog = new LanguageSelectDialog ( ) ;
2023-01-17 10:41:50 +01:00
const receiveFileDialog = new ReceiveFileDialog ( ) ;
const receiveRequestDialog = new ReceiveRequestDialog ( ) ;
2018-09-21 16:05:03 +02:00
const sendTextDialog = new SendTextDialog ( ) ;
const receiveTextDialog = new ReceiveTextDialog ( ) ;
2023-01-10 05:07:57 +01:00
const pairDeviceDialog = new PairDeviceDialog ( ) ;
2023-05-04 17:38:51 +02:00
const clearDevicesDialog = new EditPairedDevicesDialog ( ) ;
2023-09-13 18:15:01 +02:00
const publicRoomDialog = new PublicRoomDialog ( ) ;
2023-01-19 04:40:28 +01:00
const base64ZipDialog = new Base64ZipDialog ( ) ;
2018-09-21 16:05:03 +02:00
const toast = new Toast ( ) ;
const notifications = new Notifications ( ) ;
2019-03-12 23:37:50 +01:00
const networkStatusUI = new NetworkStatusUI ( ) ;
2019-03-13 01:21:44 +01:00
const webShareTargetUI = new WebShareTargetUI ( ) ;
2023-01-18 15:45:53 +01:00
const webFileHandlersUI = new WebFileHandlersUI ( ) ;
2023-01-17 14:19:51 +01:00
const noSleepUI = new NoSleepUI ( ) ;
2023-05-04 17:38:51 +02:00
const broadCast = new BrowserTabsConnector ( ) ;
2019-08-28 20:44:51 +05:30
} ) ;
2018-09-21 16:05:03 +02:00
}
}
2023-01-10 05:07:57 +01:00
const persistentStorage = new PersistentStorage ( ) ;
2023-01-17 10:50:28 +01:00
const pairDrop = new PairDrop ( ) ;
2023-07-06 21:29:36 +02:00
const localization = new Localization ( ) ;
2018-09-21 16:05:03 +02:00
2018-10-24 17:43:50 +02:00
if ( 'serviceWorker' in navigator ) {
2019-03-14 21:37:44 +01:00
navigator . serviceWorker . register ( '/service-worker.js' )
2018-09-21 18:54:52 +02:00
. then ( serviceWorker => {
console . log ( 'Service Worker registered' ) ;
window . serviceWorker = serviceWorker
} ) ;
2018-09-21 16:05:03 +02:00
}
2019-03-14 21:37:44 +01:00
window . addEventListener ( 'beforeinstallprompt' , e => {
2023-01-23 00:03:26 +01:00
if ( ! window . matchMedia ( '(display-mode: minimal-ui)' ) . matches ) {
// only display install btn when installed
2019-03-14 21:37:44 +01:00
const btn = document . querySelector ( '#install' )
btn . hidden = false ;
btn . onclick = _ => e . prompt ( ) ;
}
2023-01-23 00:03:26 +01:00
return e . preventDefault ( ) ;
2019-03-14 21:37:44 +01:00
} ) ;
2023-04-20 21:12:06 +02:00
// Background Circles
2018-09-21 16:05:03 +02:00
Events . on ( 'load' , ( ) => {
2023-05-16 18:55:36 +02:00
let c = $$ ( 'canvas' ) ;
2023-03-25 01:54:24 +01:00
let cCtx = c . getContext ( '2d' ) ;
2023-02-08 05:14:34 +01:00
let x0 , y0 , w , h , dw , offset ;
2018-09-21 16:05:03 +02:00
function init ( ) {
2023-03-25 01:54:24 +01:00
let oldW = w ;
let oldH = h ;
let oldOffset = offset
2023-03-03 12:03:20 +01:00
w = document . documentElement . clientWidth ;
h = document . documentElement . clientHeight ;
2023-09-13 18:15:01 +02:00
offset = $$ ( 'footer' ) . offsetHeight - 27 ;
if ( h > 800 ) offset += 10 ;
2023-03-25 01:54:24 +01:00
if ( oldW === w && oldH === h && oldOffset === offset ) return ; // nothing has changed
c . width = w ;
c . height = h ;
2018-09-21 16:05:03 +02:00
x0 = w / 2 ;
y0 = h - offset ;
2023-03-25 01:54:24 +01:00
dw = Math . round ( Math . max ( w , h , 1000 ) / 13 ) ;
2023-04-20 21:12:06 +02:00
drawCircles ( cCtx , dw ) ;
2018-09-21 16:05:03 +02:00
}
2023-04-20 21:12:06 +02:00
2023-03-01 10:04:37 +01:00
Events . on ( 'bg-resize' , _ => init ( ) ) ;
window . onresize = _ => Events . fire ( 'bg-resize' ) ;
2018-09-21 16:05:03 +02:00
2023-03-25 01:54:24 +01:00
function drawCircle ( ctx , radius ) {
2018-09-21 16:05:03 +02:00
ctx . beginPath ( ) ;
2023-03-25 01:54:24 +01:00
ctx . lineWidth = 2 ;
2023-04-20 21:12:06 +02:00
let opacity = 0.3 * ( 1 - 1.2 * radius / Math . max ( w , h ) ) ;
ctx . strokeStyle = ` rgba(128, 128, 128, ${ opacity } ) ` ;
2018-09-21 16:05:03 +02:00
ctx . arc ( x0 , y0 , radius , 0 , 2 * Math . PI ) ;
ctx . stroke ( ) ;
}
2023-03-25 01:54:24 +01:00
function drawCircles ( ctx , frame ) {
2023-05-16 18:55:36 +02:00
ctx . clearRect ( 0 , 0 , w , h ) ;
2023-03-25 01:54:24 +01:00
for ( let i = 0 ; i < 13 ; i ++ ) {
2023-04-20 21:12:06 +02:00
drawCircle ( ctx , dw * i + frame + 33 ) ;
2018-09-21 16:05:03 +02:00
}
}
init ( ) ;
} ) ;
2023-01-25 09:43:32 +01:00
document . changeFavicon = function ( src ) {
document . querySelector ( '[rel="icon"]' ) . href = src ;
document . querySelector ( '[rel="shortcut icon"]' ) . href = src ;
}
2023-01-17 10:50:28 +01:00
// close About PairDrop page on Escape
2022-12-30 17:09:15 +01:00
window . addEventListener ( "keydown" , ( e ) => {
if ( e . key === "Escape" ) {
window . location . hash = '#' ;
}
} ) ;
2018-09-21 16:05:03 +02:00
Notifications . PERMISSION _ERROR = `
2020-12-22 21:23:10 +01:00
Notifications permission has been blocked
as the user has dismissed the permission prompt several times .
This can be reset in Page Info
2018-09-21 16:05:03 +02:00
which can be accessed by clicking the lock icon next to the URL . ` ;