Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
coopwire-hypothesis
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
孙灵跃 Leon Sun
coopwire-hypothesis
Commits
d0963217
Unverified
Commit
d0963217
authored
May 04, 2020
by
Lyza Gardner
Committed by
GitHub
May 04, 2020
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #2103 from hypothesis/migrate-sidebar-content
Migrate `SidebarContent` to preact
parents
f2d53d7b
30389104
Changes
20
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
606 additions
and
640 deletions
+606
-640
sidebar-content.js
src/sidebar/components/sidebar-content.js
+148
-185
sidebar-content-test.js
src/sidebar/components/test/sidebar-content-test.js
+204
-353
events.js
src/sidebar/events.js
+0
-3
index.js
src/sidebar/index.js
+2
-12
frame-sync.js
src/sidebar/services/frame-sync.js
+1
-5
streamer.js
src/sidebar/services/streamer.js
+22
-1
frame-sync-test.js
src/sidebar/services/test/frame-sync-test.js
+4
-10
streamer-test.js
src/sidebar/services/test/streamer-test.js
+61
-13
activity.js
src/sidebar/store/modules/activity.js
+10
-0
direct-linked.js
src/sidebar/store/modules/direct-linked.js
+5
-0
frames.js
src/sidebar/store/modules/frames.js
+23
-10
selection.js
src/sidebar/store/modules/selection.js
+17
-1
activity-test.js
src/sidebar/store/modules/test/activity-test.js
+17
-0
direct-linked-test.js
src/sidebar/store/modules/test/direct-linked-test.js
+7
-0
frames-test.js
src/sidebar/store/modules/test/frames-test.js
+6
-1
selection-test.js
src/sidebar/store/modules/test/selection-test.js
+28
-0
viewer-test.js
src/sidebar/store/modules/test/viewer-test.js
+19
-0
viewer.js
src/sidebar/store/modules/viewer.js
+31
-2
hypothesis-app.html
src/sidebar/templates/hypothesis-app.html
+1
-5
sidebar-content.html
src/sidebar/templates/sidebar-content.html
+0
-39
No files found.
src/sidebar/components/sidebar-content.js
View file @
d0963217
import
events
from
'../events'
;
import
isThirdPartyService
from
'../util/is-third-party-service'
;
import
*
as
tabs
from
'../util/tabs'
;
// @ngInject
function
SidebarContentController
(
$scope
,
analytics
,
loadAnnotationsService
,
store
,
import
{
createElement
}
from
'preact'
;
import
propTypes
from
'prop-types'
;
import
{
useEffect
,
useRef
}
from
'preact/hooks'
;
import
{
withServices
}
from
'../util/service-context'
;
import
useStore
from
'../store/use-store'
;
import
{
tabForAnnotation
}
from
'../util/tabs'
;
import
FocusedModeHeader
from
'./focused-mode-header'
;
import
LoggedOutMessage
from
'./logged-out-message'
;
import
LoginPromptPanel
from
'./login-prompt-panel'
;
import
SearchStatusBar
from
'./search-status-bar'
;
import
SelectionTabs
from
'./selection-tabs'
;
import
SidebarContentError
from
'./sidebar-content-error'
;
import
ThreadList
from
'./thread-list'
;
/**
* Render the sidebar and its components
*/
function
SidebarContent
({
frameSync
,
rootThread
,
settings
,
streamer
)
{
const
self
=
this
;
this
.
rootThread
=
()
=>
rootThread
.
thread
(
store
.
getState
());
function
focusAnnotation
(
annotation
)
{
let
highlights
=
[];
if
(
annotation
)
{
highlights
=
[
annotation
.
$tag
];
}
frameSync
.
focusAnnotations
(
highlights
);
}
function
scrollToAnnotation
(
annotation
)
{
if
(
!
annotation
)
{
return
;
}
frameSync
.
scrollToAnnotation
(
annotation
.
$tag
);
}
/**
* Returns the Annotation object for the first annotation in the
* selected annotation set. Note that 'first' refers to the order
* of annotations passed to store when selecting annotations,
* not the order in which they appear in the document.
*/
function
firstSelectedAnnotation
()
{
const
selectedAnnotationMap
=
store
.
getSelectedAnnotationMap
();
if
(
selectedAnnotationMap
)
{
const
id
=
Object
.
keys
(
selectedAnnotationMap
)[
0
];
return
store
.
getState
().
annotations
.
annotations
.
find
(
function
(
annot
)
{
return
annot
.
id
===
id
;
});
}
else
{
return
null
;
}
}
this
.
isLoading
=
()
=>
{
if
(
!
store
.
frames
().
some
(
function
(
frame
)
{
return
frame
.
uri
;
})
)
{
// The document's URL isn't known so the document must still be loading.
return
true
;
}
return
store
.
isFetchingAnnotations
();
};
$scope
.
$on
(
'sidebarOpened'
,
function
()
{
analytics
.
track
(
analytics
.
events
.
SIDEBAR_OPENED
);
onLogin
,
onSignUp
,
loadAnnotationsService
,
rootThread
:
rootThreadService
,
streamer
,
})
{
const
rootThread
=
useStore
(
store
=>
rootThreadService
.
thread
(
store
.
getState
())
);
streamer
.
connect
();
// Store state values
const
focusedGroupId
=
useStore
(
store
=>
store
.
focusedGroupId
());
const
hasAppliedFilter
=
useStore
(
store
=>
store
.
hasAppliedFilter
());
const
isFocusedMode
=
useStore
(
store
=>
store
.
focusModeEnabled
());
const
annotationsLoading
=
useStore
(
store
=>
{
return
!
store
.
hasFetchedAnnotations
()
||
store
.
isFetchingAnnotations
();
});
const
isLoggedIn
=
useStore
(
store
=>
store
.
isLoggedIn
());
const
linkedAnnotationId
=
useStore
(
store
=>
store
.
directLinkedAnnotationId
()
);
const
linkedAnnotation
=
useStore
(
store
=>
{
return
linkedAnnotationId
?
store
.
findAnnotationByID
(
linkedAnnotationId
)
:
undefined
;
});
const
directLinkedTab
=
linkedAnnotation
?
tabForAnnotation
(
linkedAnnotation
)
:
null
;
const
searchUris
=
useStore
(
store
=>
store
.
searchUris
());
const
sidebarHasOpened
=
useStore
(
store
=>
store
.
hasSidebarOpened
());
const
userId
=
useStore
(
store
=>
store
.
profile
().
userid
);
// The local `$tag` of a direct-linked annotation; populated once it
// has anchored: meaning that it's ready to be focused and scrolled to
const
linkedAnnotationAnchorTag
=
linkedAnnotation
&&
linkedAnnotation
.
$orphan
===
false
?
linkedAnnotation
.
$tag
:
null
;
// Actions
const
clearSelectedAnnotations
=
useStore
(
store
=>
store
.
clearSelectedAnnotations
);
const
selectTab
=
useStore
(
store
=>
store
.
selectTab
);
this
.
$onInit
=
()
=>
{
// If the user is logged in, we connect nevertheless
if
(
this
.
auth
.
status
===
'logged-in'
)
{
streamer
.
connect
();
}
};
// If, after loading completes, no `linkedAnnotation` object is present when
// a `linkedAnnotationId` is set, that indicates an error
const
hasDirectLinkedAnnotationError
=
!
annotationsLoading
&&
linkedAnnotationId
?
!
linkedAnnotation
:
false
;
$scope
.
$on
(
events
.
USER_CHANGED
,
function
()
{
st
reamer
.
reconnect
();
}
);
const
hasDirectLinkedGroupError
=
useStore
(
store
=>
st
ore
.
directLinkedGroupFetchFailed
()
);
$scope
.
$on
(
events
.
ANNOTATIONS_SYNCED
,
function
(
event
,
tags
)
{
// When a direct-linked annotation is successfully anchored in the page,
// focus and scroll to it
const
selectedAnnot
=
firstSelectedAnnotation
();
if
(
!
selectedAnnot
)
{
return
;
}
const
matchesSelection
=
tags
.
some
(
function
(
tag
)
{
return
tag
===
selectedAnnot
.
$tag
;
});
if
(
!
matchesSelection
)
{
return
;
}
focusAnnotation
(
selectedAnnot
);
scrollToAnnotation
(
selectedAnnot
);
const
hasContentError
=
hasDirectLinkedAnnotationError
||
hasDirectLinkedGroupError
;
store
.
selectTab
(
tabs
.
tabForAnnotation
(
selectedAnnot
))
;
})
;
const
showTabs
=
!
hasContentError
&&
!
hasAppliedFilter
;
const
showSearchStatus
=
!
hasContentError
&&
!
annotationsLoading
;
// Re-fetch annotations when focused group, logged-in user or connected frames
// change.
$scope
.
$watch
(
()
=>
[
store
.
focusedGroupId
(),
store
.
profile
().
userid
,
...
store
.
searchUris
(),
],
([
currentGroupId
],
[
prevGroupId
])
=>
{
if
(
!
currentGroupId
)
{
// When switching accounts, groups are cleared and so the focused group id
// will be null for a brief period of time.
store
.
clearSelectedAnnotations
();
return
;
}
if
(
!
prevGroupId
||
currentGroupId
!==
prevGroupId
)
{
store
.
clearSelectedAnnotations
();
}
const
searchUris
=
store
.
searchUris
();
loadAnnotationsService
.
load
(
searchUris
,
currentGroupId
);
},
true
);
// Show a CTA to log in if successfully viewing a direct-linked annotation
// and not logged in
const
showLoggedOutMessage
=
linkedAnnotationId
&&
!
isLoggedIn
&&
!
hasDirectLinkedAnnotationError
&&
!
annotationsLoading
;
const
prevGroupId
=
useRef
(
focusedGroupId
);
this
.
showFocusedHeader
=
()
=>
{
return
store
.
focusModeEnabled
();
};
this
.
showSelectedTabs
=
function
()
{
if
(
this
.
selectedAnnotationUnavailable
()
||
this
.
selectedGroupUnavailable
()
||
store
.
getState
().
selection
.
filterQuery
)
{
return
false
;
}
else
if
(
store
.
focusModeFocused
())
{
return
false
;
}
else
{
return
true
;
// Reload annotations when group, user or document search URIs change
useEffect
(()
=>
{
if
(
!
prevGroupId
.
current
||
prevGroupId
.
current
!==
focusedGroupId
)
{
// Clear any selected annotations when the group ID changes
clearSelectedAnnotations
();
prevGroupId
.
current
=
focusedGroupId
;
}
};
this
.
setCollapsed
=
function
(
id
,
collapsed
)
{
store
.
setCollapsed
(
id
,
collapsed
);
};
this
.
focus
=
focusAnnotation
;
this
.
scrollTo
=
scrollToAnnotation
;
this
.
selectedGroupUnavailable
=
function
()
{
return
store
.
getState
().
directLinked
.
directLinkedGroupFetchFailed
;
};
this
.
selectedAnnotationUnavailable
=
function
()
{
const
selectedID
=
store
.
getFirstSelectedAnnotationId
();
return
(
!
this
.
isLoading
()
&&
!!
selectedID
&&
!
store
.
annotationExists
(
selectedID
)
);
};
this
.
shouldShowLoggedOutMessage
=
function
()
{
// If user is not logged out, don't show CTA.
if
(
self
.
auth
.
status
!==
'logged-out'
)
{
return
false
;
if
(
focusedGroupId
&&
searchUris
.
length
)
{
loadAnnotationsService
.
load
(
searchUris
,
focusedGroupId
);
}
// If user has not landed on a direct linked annotation
// don't show the CTA.
if
(
!
store
.
getState
().
directLinked
.
directLinkedAnnotationId
)
{
return
false
;
},
[
clearSelectedAnnotations
,
loadAnnotationsService
,
focusedGroupId
,
userId
,
searchUris
,
]);
// When a `linkedAnnotationAnchorTag` becomes available, scroll to it
// and focus it
useEffect
(()
=>
{
if
(
linkedAnnotationAnchorTag
)
{
frameSync
.
focusAnnotations
([
linkedAnnotationAnchorTag
]);
frameSync
.
scrollToAnnotation
(
linkedAnnotationAnchorTag
);
selectTab
(
directLinkedTab
);
}
},
[
directLinkedTab
,
frameSync
,
linkedAnnotationAnchorTag
,
selectTab
]);
// The CTA text and links are only applicable when using Hypothesis
// accounts.
if
(
isThirdPartyService
(
settings
)
)
{
return
false
;
// Connect to the streamer when the sidebar has opened or if user is logged in
useEffect
(()
=>
{
if
(
sidebarHasOpened
||
isLoggedIn
)
{
streamer
.
connect
()
;
}
// The user is logged out and has landed on a direct linked
// annotation. If there is an annotation selection and that
// selection is available to the user, show the CTA.
const
selectedID
=
store
.
getFirstSelectedAnnotationId
();
return
(
!
store
.
isFetchingAnnotations
()
&&
!!
selectedID
&&
store
.
annotationExists
(
selectedID
)
);
};
},
[
streamer
,
sidebarHasOpened
,
isLoggedIn
]);
return
(
<
div
>
{
isFocusedMode
&&
<
FocusedModeHeader
/>
}
<
LoginPromptPanel
onLogin
=
{
onLogin
}
onSignUp
=
{
onSignUp
}
/
>
{
hasDirectLinkedAnnotationError
&&
(
<
SidebarContentError
errorType
=
"annotation"
onLoginRequest
=
{
onLogin
}
/
>
)}
{
hasDirectLinkedGroupError
&&
(
<
SidebarContentError
errorType
=
"group"
onLoginRequest
=
{
onLogin
}
/
>
)}
{
showTabs
&&
<
SelectionTabs
isLoading
=
{
annotationsLoading
}
/>
}
{
showSearchStatus
&&
<
SearchStatusBar
/>
}
<
ThreadList
thread
=
{
rootThread
}
/
>
{
showLoggedOutMessage
&&
<
LoggedOutMessage
onLogin
=
{
onLogin
}
/>
}
<
/div
>
);
}
export
default
{
controller
:
SidebarContentController
,
controllerAs
:
'vm'
,
bindings
:
{
auth
:
'<'
,
onLogin
:
'&'
,
onSignUp
:
'&'
,
},
template
:
require
(
'../templates/sidebar-content.html'
),
SidebarContent
.
propTypes
=
{
// Callbacks for log in and out
onLogin
:
propTypes
.
func
.
isRequired
,
onSignUp
:
propTypes
.
func
.
isRequired
,
// Injected
frameSync
:
propTypes
.
object
,
loadAnnotationsService
:
propTypes
.
object
,
rootThread
:
propTypes
.
object
,
streamer
:
propTypes
.
object
,
};
SidebarContent
.
injectedProps
=
[
'frameSync'
,
'loadAnnotationsService'
,
'rootThread'
,
'streamer'
,
];
export
default
withServices
(
SidebarContent
);
src/sidebar/components/test/sidebar-content-test.js
View file @
d0963217
import
angular
from
'angular'
;
import
EventEmitter
from
'tiny-emitter'
;
import
events
from
'../../events'
;
import
storeFactory
from
'../../store'
;
import
sidebarContent
from
'../sidebar-content'
;
class
FakeRootThread
extends
EventEmitter
{
constructor
()
{
super
();
this
.
thread
=
sinon
.
stub
().
returns
({
totalChildren
:
0
,
});
}
}
describe
(
'sidebar.components.sidebar-content'
,
function
()
{
let
$rootScope
;
let
$scope
;
let
store
;
let
ctrl
;
let
fakeAnalytics
;
let
fakeLoadAnnotationsService
;
let
fakeFrameSync
;
let
fakeRootThread
;
let
fakeSettings
;
let
fakeStreamer
;
let
sandbox
;
import
{
mount
}
from
'enzyme'
;
import
{
createElement
}
from
'preact'
;
before
(
function
()
{
angular
.
module
(
'h'
,
[])
.
service
(
'store'
,
storeFactory
)
.
component
(
'sidebarContent'
,
sidebarContent
);
});
import
SidebarContent
from
'../sidebar-content'
;
import
{
$imports
}
from
'../sidebar-content'
;
beforeEach
(
angular
.
mock
.
module
(
'h'
));
beforeEach
(()
=>
{
angular
.
mock
.
module
(
function
(
$provide
)
{
sandbox
=
sinon
.
createSandbox
();
fakeAnalytics
=
{
track
:
sandbox
.
stub
(),
events
:
{},
};
fakeFrameSync
=
{
focusAnnotations
:
sinon
.
stub
(),
scrollToAnnotation
:
sinon
.
stub
(),
};
fakeStreamer
=
{
setConfig
:
sandbox
.
stub
(),
connect
:
sandbox
.
stub
(),
reconnect
:
sandbox
.
stub
(),
};
fakeLoadAnnotationsService
=
{
load
:
sinon
.
stub
(),
};
fakeRootThread
=
new
FakeRootThread
();
fakeSettings
=
{};
$provide
.
value
(
'analytics'
,
fakeAnalytics
);
$provide
.
value
(
'frameSync'
,
fakeFrameSync
);
$provide
.
value
(
'rootThread'
,
fakeRootThread
);
$provide
.
value
(
'streamer'
,
fakeStreamer
);
$provide
.
value
(
'loadAnnotationsService'
,
fakeLoadAnnotationsService
);
$provide
.
value
(
'settings'
,
fakeSettings
);
});
});
import
{
checkAccessibility
}
from
'../../../test-util/accessibility'
;
import
mockImportedComponents
from
'../../../test-util/mock-imported-components'
;
function
setFrames
(
frames
)
{
frames
.
forEach
(
function
(
frame
)
{
store
.
connectFrame
(
frame
);
});
}
const
makeSidebarContentController
=
()
=>
{
angular
.
mock
.
inject
(
function
(
$componentController
,
_store_
,
_$rootScope_
)
{
$rootScope
=
_$rootScope_
;
$scope
=
$rootScope
.
$new
();
store
=
_store_
;
store
.
updateFrameAnnotationFetchStatus
=
sinon
.
stub
();
store
.
clearGroups
();
store
.
loadGroups
([{
id
:
'group-id'
}]);
store
.
focusGroup
(
'group-id'
);
ctrl
=
$componentController
(
'sidebarContent'
,
{
$scope
:
$scope
},
{
auth
:
{
status
:
'unknown'
},
}
);
});
};
describe
(
'SidebarContent'
,
()
=>
{
let
fakeFrameSync
;
let
fakeLoadAnnotationsService
;
let
fakeRootThreadService
;
let
fakeStore
;
let
fakeStreamer
;
let
fakeTabsUtil
;
const
createComponent
=
props
=>
mount
(
<
SidebarContent
onLogin
=
{()
=>
null
}
onSignUp
=
{()
=>
null
}
frameSync
=
{
fakeFrameSync
}
loadAnnotationsService
=
{
fakeLoadAnnotationsService
}
rootThread
=
{
fakeRootThreadService
}
streamer
=
{
fakeStreamer
}
{...
props
}
/
>
);
beforeEach
(()
=>
{
makeSidebarContentController
();
});
afterEach
(
function
()
{
return
sandbox
.
restore
();
});
describe
(
'isLoading'
,
()
=>
{
it
(
"returns true if the document's url isn't known"
,
()
=>
{
assert
.
isTrue
(
ctrl
.
isLoading
());
});
it
(
'returns true if annotations are still being fetched'
,
()
=>
{
setFrames
([{
uri
:
'http://www.example.com'
}]);
store
.
annotationFetchStarted
(
'tag:foo'
);
assert
.
isTrue
(
ctrl
.
isLoading
());
fakeFrameSync
=
{
focusAnnotations
:
sinon
.
stub
(),
scrollToAnnotation
:
sinon
.
stub
(),
};
fakeLoadAnnotationsService
=
{
load
:
sinon
.
stub
(),
};
fakeRootThreadService
=
{
thread
:
sinon
.
stub
().
returns
({}),
};
fakeStreamer
=
{
connect
:
sinon
.
stub
(),
};
fakeStore
=
{
// actions
clearSelectedAnnotations
:
sinon
.
stub
(),
selectTab
:
sinon
.
stub
(),
// selectors
annotationExists
:
sinon
.
stub
(),
directLinkedAnnotationId
:
sinon
.
stub
(),
directLinkedGroupFetchFailed
:
sinon
.
stub
(),
findAnnotationByID
:
sinon
.
stub
(),
focusedGroupId
:
sinon
.
stub
(),
focusModeEnabled
:
sinon
.
stub
(),
hasAppliedFilter
:
sinon
.
stub
(),
hasFetchedAnnotations
:
sinon
.
stub
(),
hasSidebarOpened
:
sinon
.
stub
(),
isFetchingAnnotations
:
sinon
.
stub
(),
isLoggedIn
:
sinon
.
stub
(),
getState
:
sinon
.
stub
(),
profile
:
sinon
.
stub
().
returns
({
userid
:
null
}),
searchUris
:
sinon
.
stub
().
returns
([]),
};
fakeTabsUtil
=
{
tabForAnnotation
:
sinon
.
stub
().
returns
(
'annotation'
),
};
$imports
.
$mock
(
mockImportedComponents
());
$imports
.
$mock
({
'../store/use-store'
:
callback
=>
callback
(
fakeStore
),
'../util/tabs'
:
fakeTabsUtil
,
});
});
it
(
'returns false if annotations have been fetched'
,
()
=>
{
setFrames
([{
uri
:
'http://www.example.com'
}]);
assert
.
isFalse
(
ctrl
.
isLoading
());
});
afterEach
(()
=>
{
$imports
.
$restore
();
});
describe
(
'showSelectedTabs'
,
()
=>
{
describe
(
'loading annotations'
,
()
=>
{
let
wrapper
;
beforeEach
(()
=>
{
setFrames
([{
uri
:
'http://www.example.com'
}]);
});
it
(
'returns false if there is a search query'
,
()
=>
{
store
.
setFilterQuery
(
'tag:foo'
);
assert
.
isFalse
(
ctrl
.
showSelectedTabs
());
fakeStore
.
focusedGroupId
.
returns
(
'47'
);
fakeStore
.
searchUris
.
returns
([
'foobar'
]);
fakeStore
.
profile
.
returns
({
userid
:
'somebody'
});
wrapper
=
createComponent
();
fakeLoadAnnotationsService
.
load
.
resetHistory
();
});
it
(
'
returns false if selected group is unavailable
'
,
()
=>
{
fakeS
ettings
.
group
=
'group-id'
;
store
.
setDirectLinkedGroupFetchFailed
(
);
$scope
.
$digest
(
);
assert
.
isFalse
(
ctrl
.
showSelectedTabs
()
);
it
(
'
loads annotations when userId changes
'
,
()
=>
{
fakeS
tore
.
profile
.
returns
({
userid
:
'somethingElse'
})
;
wrapper
.
setProps
({}
);
assert
.
calledOnce
(
fakeLoadAnnotationsService
.
load
);
assert
.
notCalled
(
fakeStore
.
clearSelectedAnnotations
);
});
it
(
'returns false if selected annotation is unavailable'
,
()
=>
{
store
.
selectAnnotations
([
'missing'
]);
$scope
.
$digest
();
assert
.
isFalse
(
ctrl
.
showSelectedTabs
());
it
(
'clears selected annotations and loads annotations when groupId changes'
,
()
=>
{
fakeStore
.
focusedGroupId
.
returns
(
'affable'
);
wrapper
.
setProps
({});
assert
.
calledOnce
(
fakeLoadAnnotationsService
.
load
);
assert
.
calledOnce
(
fakeStore
.
clearSelectedAnnotations
);
});
it
(
'returns true in all other cases'
,
()
=>
{
assert
.
isTrue
(
ctrl
.
showSelectedTabs
());
it
(
'loads annotations when searchURIs change'
,
()
=>
{
fakeStore
.
searchUris
.
returns
([
'abandon-ship'
]);
wrapper
.
setProps
({});
assert
.
calledOnce
(
fakeLoadAnnotationsService
.
load
);
assert
.
notCalled
(
fakeStore
.
clearSelectedAnnotations
);
});
});
describe
(
'showFocusedHeader'
,
()
=>
{
it
(
'returns true if focus mode is enabled'
,
()
=>
{
store
.
focusModeEnabled
=
sinon
.
stub
().
returns
(
true
);
assert
.
isTrue
(
ctrl
.
showFocusedHeader
());
});
it
(
'returns false if focus mode is not enabled'
,
()
=>
{
store
.
focusModeEnabled
=
sinon
.
stub
().
returns
(
false
);
assert
.
isFalse
(
ctrl
.
showFocusedHeader
());
});
});
function
connectFrameAndPerformInitialFetch
()
{
setFrames
([{
uri
:
'https://a-page.com'
}]);
$scope
.
$digest
();
fakeLoadAnnotationsService
.
load
.
reset
();
}
it
(
'generates the thread list'
,
()
=>
{
const
thread
=
fakeRootThread
.
thread
(
store
.
getState
());
assert
.
equal
(
ctrl
.
rootThread
(),
thread
);
});
context
(
'when the search URIs of connected frames change'
,
()
=>
{
beforeEach
(
connectFrameAndPerformInitialFetch
);
context
(
'when viewing a direct-linked annotation'
,
()
=>
{
context
(
'successful direct-linked annotation'
,
()
=>
{
beforeEach
(()
=>
{
fakeStore
.
hasFetchedAnnotations
.
returns
(
true
);
fakeStore
.
isFetchingAnnotations
.
returns
(
false
);
fakeStore
.
annotationExists
.
withArgs
(
'someId'
).
returns
(
true
);
fakeStore
.
directLinkedAnnotationId
.
returns
(
'someId'
);
fakeStore
.
findAnnotationByID
.
withArgs
(
'someId'
)
.
returns
({
$orphan
:
false
,
$tag
:
'myTag'
});
});
it
(
'reloads annotations'
,
()
=>
{
setFrames
([{
uri
:
'https://new-frame.com'
}]);
it
(
'focuses and scrolls to direct-linked annotations once anchored'
,
()
=>
{
createComponent
();
assert
.
calledOnce
(
fakeFrameSync
.
scrollToAnnotation
);
assert
.
calledWith
(
fakeFrameSync
.
scrollToAnnotation
,
'myTag'
);
assert
.
calledOnce
(
fakeFrameSync
.
focusAnnotations
);
assert
.
calledWith
(
fakeFrameSync
.
focusAnnotations
,
sinon
.
match
([
'myTag'
])
);
});
$scope
.
$digest
();
it
(
'selects the correct tab for direct-linked annotations once anchored'
,
()
=>
{
createComponent
();
assert
.
calledOnce
(
fakeStore
.
selectTab
);
assert
.
calledWith
(
fakeStore
.
selectTab
,
'annotation'
);
});
assert
.
calledWith
(
fake
LoadAnnotationsService
.
load
,
[
'https://a-page.com'
,
'https://new-frame.com'
],
'group-id'
);
it
(
'renders a logged-out message CTA if user is not logged in'
,
()
=>
{
fake
Store
.
isLoggedIn
.
returns
(
false
);
const
wrapper
=
createComponent
();
assert
.
isTrue
(
wrapper
.
find
(
'LoggedOutMessage'
).
exists
());
}
);
});
});
context
(
'when the profile changes'
,
()
=>
{
beforeEach
(
connectFrameAndPerformInitialFetch
);
context
(
'error on direct-linked annotation'
,
()
=>
{
beforeEach
(()
=>
{
// This puts us into a "direct-linked annotation" state
fakeStore
.
hasFetchedAnnotations
.
returns
(
true
);
fakeStore
.
isFetchingAnnotations
.
returns
(
false
);
fakeStore
.
directLinkedAnnotationId
.
returns
(
'someId'
);
it
(
'reloads annotations if the user ID changed'
,
()
=>
{
const
newProfile
=
Object
.
assign
({},
store
.
profile
(),
{
userid
:
'different-user@hypothes.is'
,
// This puts us into an error state
fakeStore
.
findAnnotationByID
.
withArgs
(
'someId'
).
returns
(
undefined
);
fakeStore
.
annotationExists
.
withArgs
(
'someId'
).
returns
(
false
);
});
store
.
updateProfile
(
newProfile
);
$scope
.
$diges
t
();
it
(
'renders a content error'
,
()
=>
{
const
wrapper
=
createComponen
t
();
assert
.
calledWith
(
fakeLoadAnnotationsService
.
load
,
[
'https://a-page.com'
],
'group-id'
);
});
it
(
'does not reload annotations if the user ID is the same'
,
()
=>
{
const
newProfile
=
Object
.
assign
({},
store
.
profile
(),
{
user_info
:
{
display_name
:
'New display name'
,
},
assert
.
isTrue
(
wrapper
.
find
(
'SidebarContentError'
)
.
filter
({
errorType
:
'annotation'
})
.
exists
()
);
});
store
.
updateProfile
(
newProfile
);
$scope
.
$diges
t
();
it
(
'does not render tabs'
,
()
=>
{
const
wrapper
=
createComponen
t
();
assert
.
notCalled
(
fakeLoadAnnotationsService
.
load
);
});
});
describe
(
'when an annotation is anchored'
,
function
()
{
it
(
'focuses and scrolls to the annotation if already selected'
,
function
()
{
const
uri
=
'http://example.com'
;
store
.
getSelectedAnnotationMap
=
sinon
.
stub
().
returns
({
'123'
:
true
});
setFrames
([{
uri
:
uri
}]);
const
annot
=
{
$tag
:
'atag'
,
id
:
'123'
,
};
store
.
addAnnotations
([
annot
]);
$scope
.
$digest
();
$rootScope
.
$broadcast
(
events
.
ANNOTATIONS_SYNCED
,
[
'atag'
]);
assert
.
calledWith
(
fakeFrameSync
.
focusAnnotations
,
[
'atag'
]);
assert
.
calledWith
(
fakeFrameSync
.
scrollToAnnotation
,
'atag'
);
assert
.
isFalse
(
wrapper
.
find
(
'SelectionTabs'
).
exists
());
});
});
});
describe
(
'when the focused group changes'
,
()
=>
{
const
uri
=
'http://example.com'
;
context
(
'error with direct-linked group'
,
()
=>
{
beforeEach
(()
=>
{
// Setup an initial state with frames connected, a group focused and some
// annotations loaded.
store
.
addAnnotations
([{
id
:
'123'
}]);
store
.
addAnnotations
=
sinon
.
stub
();
setFrames
([{
uri
:
uri
}]);
$scope
.
$digest
();
fakeLoadAnnotationsService
.
load
=
sinon
.
stub
();
fakeStore
.
hasFetchedAnnotations
.
returns
(
true
);
fakeStore
.
isFetchingAnnotations
.
returns
(
false
);
fakeStore
.
directLinkedGroupFetchFailed
.
returns
(
true
);
});
it
(
'should load annotations for the new group'
,
()
=>
{
store
.
loadGroups
([{
id
:
'different-group'
}]);
store
.
focusGroup
(
'different-group'
);
it
(
'renders a content error'
,
()
=>
{
const
wrapper
=
createComponent
();
$scope
.
$digest
();
assert
.
calledWith
(
fakeLoadAnnotationsService
.
load
,
[
'http://example.com'
],
'different-group'
assert
.
isTrue
(
wrapper
.
find
(
'SidebarContentError'
)
.
filter
({
errorType
:
'group'
})
.
exists
()
);
});
it
(
'should clear the selection'
,
()
=>
{
store
.
selectAnnotations
([
'123'
]);
store
.
loadGroups
([{
id
:
'different-group'
}]);
store
.
focusGroup
(
'different-group'
);
$scope
.
$digest
();
it
(
'does not render tabs'
,
()
=>
{
const
wrapper
=
createComponent
();
assert
.
isFalse
(
store
.
hasSelectedAnnotation
s
());
assert
.
isFalse
(
wrapper
.
find
(
'SelectionTabs'
).
exist
s
());
});
});
describe
(
'direct linking messages'
,
function
()
{
/**
* Connect a frame, indicating that the document has finished initial
* loading.
*
* In the case of an HTML document, this usually happens immediately. For
* PDFs, this happens once the entire PDF has been downloaded and the
* document's metadata has been read.
*/
function
addFrame
()
{
setFrames
([
{
uri
:
'http://www.example.com'
,
},
]);
}
beforeEach
(
function
()
{
store
.
setDirectLinkedAnnotationId
(
'test'
);
});
it
(
'displays a message if the selection is unavailable'
,
function
()
{
addFrame
();
store
.
selectAnnotations
([
'missing'
]);
$scope
.
$digest
();
assert
.
isTrue
(
ctrl
.
selectedAnnotationUnavailable
());
describe
(
'streamer'
,
()
=>
{
it
(
'connects to streamer when sidebar is opened'
,
()
=>
{
const
wrapper
=
createComponent
();
fakeStreamer
.
connect
.
resetHistory
();
fakeStore
.
hasSidebarOpened
.
returns
(
true
);
wrapper
.
setProps
({});
assert
.
calledOnce
(
fakeStreamer
.
connect
);
});
it
(
'
does not show a message if the selection is available'
,
function
()
{
addFrame
();
store
.
addAnnotations
([{
id
:
'123'
}]
);
store
.
selectAnnotations
([
'123'
]
);
$scope
.
$digest
(
);
assert
.
isFalse
(
ctrl
.
selectedAnnotationUnavailable
()
);
it
(
'
connects to streamer when user logs in'
,
()
=>
{
const
wrapper
=
createComponent
();
fakeStreamer
.
connect
.
resetHistory
(
);
fakeStore
.
isLoggedIn
.
returns
(
true
);
wrapper
.
setProps
({}
);
assert
.
calledOnce
(
fakeStreamer
.
connect
);
});
});
it
(
'does not a show a message if there is no selection'
,
function
()
{
addFrame
();
store
.
selectAnnotations
([]);
$scope
.
$digest
();
assert
.
isFalse
(
ctrl
.
selectedAnnotationUnavailable
());
});
it
(
'renders a focused header if in focused mode'
,
()
=>
{
fakeStore
.
focusModeEnabled
.
returns
(
true
);
const
wrapper
=
createComponent
();
it
(
"doesn't show a message if the document isn't loaded yet"
,
function
()
{
// There is a selection but the selected annotation isn't available.
store
.
selectAnnotations
([
'missing'
]);
store
.
annotationFetchStarted
();
$scope
.
$digest
();
assert
.
isTrue
(
wrapper
.
find
(
'FocusedModeHeader'
).
exists
());
});
assert
.
isFalse
(
ctrl
.
selectedAnnotationUnavailable
());
});
it
(
'renders search status'
,
()
=>
{
fakeStore
.
hasFetchedAnnotations
.
returns
(
true
);
fakeStore
.
isFetchingAnnotations
.
returns
(
false
);
it
(
'shows logged out message if selection is available'
,
function
()
{
addFrame
();
ctrl
.
auth
=
{
status
:
'logged-out'
,
};
store
.
addAnnotations
([{
id
:
'123'
}]);
store
.
selectAnnotations
([
'123'
]);
$scope
.
$digest
();
assert
.
isTrue
(
ctrl
.
shouldShowLoggedOutMessage
());
});
const
wrapper
=
createComponent
();
it
(
'does not show loggedout message if selection is unavailable'
,
function
()
{
addFrame
();
ctrl
.
auth
=
{
status
:
'logged-out'
,
};
store
.
selectAnnotations
([
'missing'
]);
$scope
.
$digest
();
assert
.
isFalse
(
ctrl
.
shouldShowLoggedOutMessage
());
});
assert
.
isTrue
(
wrapper
.
find
(
'SearchStatusBar'
).
exists
());
});
it
(
'does not show loggedout message if there is no selection'
,
function
()
{
addFrame
();
ctrl
.
auth
=
{
status
:
'logged-out'
,
};
store
.
selectAnnotations
([]);
$scope
.
$digest
();
assert
.
isFalse
(
ctrl
.
shouldShowLoggedOutMessage
());
});
it
(
'does not render search status if annotations are loading'
,
()
=>
{
fakeStore
.
hasFetchedAnnotations
.
returns
(
false
);
it
(
'does not show loggedout message if user is not logged out'
,
function
()
{
addFrame
();
ctrl
.
auth
=
{
status
:
'logged-in'
,
};
store
.
addAnnotations
([{
id
:
'123'
}]);
store
.
selectAnnotations
([
'123'
]);
$scope
.
$digest
();
assert
.
isFalse
(
ctrl
.
shouldShowLoggedOutMessage
());
});
const
wrapper
=
createComponent
();
it
(
'does not show loggedout message if not a direct link'
,
function
()
{
addFrame
();
ctrl
.
auth
=
{
status
:
'logged-out'
,
};
store
.
setDirectLinkedAnnotationId
(
null
);
store
.
addAnnotations
([{
id
:
'123'
}]);
store
.
selectAnnotations
([
'123'
]);
$scope
.
$digest
();
assert
.
isFalse
(
ctrl
.
shouldShowLoggedOutMessage
());
});
assert
.
isFalse
(
wrapper
.
find
(
'SearchStatusBar'
).
exists
());
});
it
(
'does not show loggedout message if using third-party accounts'
,
function
()
{
fakeSettings
.
services
=
[{
authority
:
'publisher.com'
}];
addFrame
();
ctrl
.
auth
=
{
status
:
'logged-out'
};
store
.
addAnnotations
([{
id
:
'123'
}]);
store
.
selectAnnotations
([
'123'
]);
$scope
.
$digest
();
describe
(
'selection tabs'
,
()
=>
{
it
(
'renders tabs'
,
()
=>
{
const
wrapper
=
createComponent
();
assert
.
is
False
(
ctrl
.
shouldShowLoggedOutMessage
());
assert
.
is
True
(
wrapper
.
find
(
'SelectionTabs'
).
exists
());
});
});
describe
(
'deferred websocket connection'
,
function
()
{
it
(
'should connect the websocket the first time the sidebar opens'
,
function
()
{
$rootScope
.
$broadcast
(
'sidebarOpened'
);
assert
.
called
(
fakeStreamer
.
connect
);
});
it
(
'does not render tabs if there is an applied filter'
,
()
=>
{
fakeStore
.
hasAppliedFilter
.
returns
(
true
);
describe
(
'when logged in user changes'
,
function
()
{
it
(
'should not reconnect if the sidebar is closed'
,
function
()
{
$rootScope
.
$broadcast
(
events
.
USER_CHANGED
);
assert
.
calledOnce
(
fakeStreamer
.
reconnect
);
});
const
wrapper
=
createComponent
();
it
(
'should reconnect if the sidebar is open'
,
function
()
{
$rootScope
.
$broadcast
(
'sidebarOpened'
);
fakeStreamer
.
connect
.
reset
();
$rootScope
.
$broadcast
(
events
.
USER_CHANGED
);
assert
.
called
(
fakeStreamer
.
reconnect
);
});
assert
.
isFalse
(
wrapper
.
find
(
'SelectionTabs'
).
exists
());
});
});
it
(
'should pass a11y checks'
,
checkAccessibility
({
content
:
()
=>
createComponent
(),
})
);
});
src/sidebar/events.js
View file @
d0963217
...
...
@@ -19,7 +19,4 @@ export default {
/** A new annotation has been created locally. */
BEFORE_ANNOTATION_CREATED
:
'beforeAnnotationCreated'
,
/** Annotations were anchored in a connected document. */
ANNOTATIONS_SYNCED
:
'sync'
,
};
src/sidebar/index.js
View file @
d0963217
...
...
@@ -108,14 +108,10 @@ registerIcons(iconSet);
// Preact UI components that are wrapped for use within Angular templates.
import
AnnotationViewerContent
from
'./components/annotation-viewer-content'
;
import
FocusedModeHeader
from
'./components/focused-mode-header'
;
import
HelpPanel
from
'./components/help-panel'
;
import
LoggedOutMessage
from
'./components/logged-out-message'
;
import
LoginPromptPanel
from
'./components/login-prompt-panel'
;
import
SearchStatusBar
from
'./components/search-status-bar'
;
import
SelectionTabs
from
'./components/selection-tabs'
;
import
ShareAnnotationsPanel
from
'./components/share-annotations-panel'
;
import
SidebarContent
Error
from
'./components/sidebar-content-error
'
;
import
SidebarContent
from
'./components/sidebar-content
'
;
import
StreamContent
from
'./components/stream-content'
;
import
ThreadList
from
'./components/thread-list'
;
import
ToastMessages
from
'./components/toast-messages'
;
...
...
@@ -124,7 +120,6 @@ import TopBar from './components/top-bar';
// Remaining UI components that are still built with Angular.
import
hypothesisApp
from
'./components/hypothesis-app'
;
import
sidebarContent
from
'./components/sidebar-content'
;
// Services.
...
...
@@ -243,12 +238,7 @@ function startAngularApp(config) {
)
.
component
(
'helpPanel'
,
wrapComponent
(
HelpPanel
))
.
component
(
'loginPromptPanel'
,
wrapComponent
(
LoginPromptPanel
))
.
component
(
'loggedOutMessage'
,
wrapComponent
(
LoggedOutMessage
))
.
component
(
'searchStatusBar'
,
wrapComponent
(
SearchStatusBar
))
.
component
(
'focusedModeHeader'
,
wrapComponent
(
FocusedModeHeader
))
.
component
(
'selectionTabs'
,
wrapComponent
(
SelectionTabs
))
.
component
(
'sidebarContent'
,
sidebarContent
)
.
component
(
'sidebarContentError'
,
wrapComponent
(
SidebarContentError
))
.
component
(
'sidebarContent'
,
wrapComponent
(
SidebarContent
))
.
component
(
'shareAnnotationsPanel'
,
wrapComponent
(
ShareAnnotationsPanel
))
.
component
(
'streamContent'
,
wrapComponent
(
StreamContent
))
.
component
(
'threadList'
,
wrapComponent
(
ThreadList
))
...
...
src/sidebar/services/frame-sync.js
View file @
d0963217
...
...
@@ -144,10 +144,6 @@ export default function FrameSync($rootScope, $window, store, bridge) {
let
anchoringStatusUpdates
=
{};
const
scheduleAnchoringStatusUpdate
=
debounce
(()
=>
{
store
.
updateAnchorStatus
(
anchoringStatusUpdates
);
$rootScope
.
$broadcast
(
events
.
ANNOTATIONS_SYNCED
,
Object
.
keys
(
anchoringStatusUpdates
)
);
anchoringStatusUpdates
=
{};
},
10
);
...
...
@@ -176,7 +172,7 @@ export default function FrameSync($rootScope, $window, store, bridge) {
});
bridge
.
on
(
'sidebarOpened'
,
function
()
{
$rootScope
.
$broadcast
(
'sidebarOpened'
);
store
.
setSidebarOpened
(
true
);
});
// These invoke the matching methods by name on the Guests
...
...
src/sidebar/services/streamer.js
View file @
d0963217
...
...
@@ -3,6 +3,7 @@ import * as queryString from 'query-string';
import
warnOnce
from
'../../shared/warn-once'
;
import
{
generateHexString
}
from
'../util/random'
;
import
Socket
from
'../websocket'
;
import
{
watch
}
from
'../util/watch'
;
/**
* Open a new WebSocket connection to the Hypothesis push notification service.
...
...
@@ -160,6 +161,26 @@ export default function Streamer(store, auth, groups, session, settings) {
});
};
let
reconnectSetUp
=
false
;
/**
* Set up automatic reconnecting when user changes.
*/
function
setUpAutoReconnect
()
{
if
(
reconnectSetUp
)
{
return
;
}
reconnectSetUp
=
true
;
// Reconnect when user changes, as auth token will have changed
watch
(
store
.
subscribe
,
()
=>
store
.
profile
().
userid
,
()
=>
{
reconnect
();
}
);
}
/**
* Connect to the Hypothesis real time update service.
*
...
...
@@ -169,10 +190,10 @@ export default function Streamer(store, auth, groups, session, settings) {
* process has started.
*/
function
connect
()
{
setUpAutoReconnect
();
if
(
socket
)
{
return
Promise
.
resolve
();
}
return
_connect
();
}
...
...
src/sidebar/services/test/frame-sync-test.js
View file @
d0963217
...
...
@@ -66,6 +66,7 @@ describe('sidebar/services/frame-sync', function () {
openSidebarPanel
:
sinon
.
stub
(),
selectAnnotations
:
sinon
.
stub
(),
selectTab
:
sinon
.
stub
(),
setSidebarOpened
:
sinon
.
stub
(),
toggleSelectedAnnotations
:
sinon
.
stub
(),
updateAnchorStatus
:
sinon
.
stub
(),
}
...
...
@@ -305,14 +306,6 @@ describe('sidebar/services/frame-sync', function () {
t2
:
'orphan'
,
});
});
it
(
'emits an ANNOTATIONS_SYNCED event'
,
function
()
{
fakeBridge
.
emit
(
'sync'
,
[{
tag
:
't1'
,
msg
:
{
$orphan
:
false
}
}]);
expireDebounceTimeout
();
assert
.
calledWith
(
$rootScope
.
$broadcast
,
events
.
ANNOTATIONS_SYNCED
,
[
't1'
,
]);
});
});
context
(
'when a new frame connects'
,
function
()
{
...
...
@@ -376,9 +369,10 @@ describe('sidebar/services/frame-sync', function () {
});
describe
(
'on "sidebarOpened" message'
,
function
()
{
it
(
'
broadcasts a sidebarOpened event
'
,
function
()
{
it
(
'
sets the sidebar open in the store
'
,
function
()
{
fakeBridge
.
emit
(
'sidebarOpened'
);
assert
.
calledWith
(
$rootScope
.
$broadcast
,
'sidebarOpened'
);
assert
.
calledWith
(
fakeStore
.
setSidebarOpened
,
true
);
});
});
...
...
src/sidebar/services/test/streamer-test.js
View file @
d0963217
import
EventEmitter
from
'tiny-emitter'
;
import
fakeReduxStore
from
'../../test/fake-redux-store'
;
import
Streamer
from
'../streamer'
;
import
{
$imports
}
from
'../streamer'
;
...
...
@@ -43,12 +44,14 @@ const fixtures = {
// the most recently created FakeSocket instance
let
fakeWebSocket
=
null
;
let
fakeWebSockets
=
[];
class
FakeSocket
extends
EventEmitter
{
constructor
(
url
)
{
super
();
fakeWebSocket
=
this
;
// eslint-disable-line consistent-this
fakeWebSockets
.
push
(
this
);
this
.
url
=
url
;
this
.
messages
=
[];
...
...
@@ -95,19 +98,22 @@ describe('Streamer', function () {
},
};
fakeStore
=
{
addAnnotations
:
sinon
.
stub
(),
annotationExists
:
sinon
.
stub
().
returns
(
false
),
clearPendingUpdates
:
sinon
.
stub
(),
pendingUpdates
:
sinon
.
stub
().
returns
({}),
pendingDeletions
:
sinon
.
stub
().
returns
({}),
profile
:
sinon
.
stub
().
returns
({
userid
:
'jim@hypothes.is'
,
}),
receiveRealTimeUpdates
:
sinon
.
stub
(),
removeAnnotations
:
sinon
.
stub
(),
route
:
sinon
.
stub
().
returns
(
'sidebar'
),
};
fakeStore
=
fakeReduxStore
(
{},
{
addAnnotations
:
sinon
.
stub
(),
annotationExists
:
sinon
.
stub
().
returns
(
false
),
clearPendingUpdates
:
sinon
.
stub
(),
pendingUpdates
:
sinon
.
stub
().
returns
({}),
pendingDeletions
:
sinon
.
stub
().
returns
({}),
profile
:
sinon
.
stub
().
returns
({
userid
:
'jim@hypothes.is'
,
}),
receiveRealTimeUpdates
:
sinon
.
stub
(),
removeAnnotations
:
sinon
.
stub
(),
route
:
sinon
.
stub
().
returns
(
'sidebar'
),
}
);
fakeGroups
=
{
focused
:
sinon
.
stub
().
returns
({
id
:
'public'
}),
...
...
@@ -130,6 +136,7 @@ describe('Streamer', function () {
afterEach
(
function
()
{
$imports
.
$restore
();
activeStreamer
=
null
;
fakeWebSockets
=
[];
});
it
(
'should not create a websocket connection if websocketUrl is not provided'
,
function
()
{
...
...
@@ -246,6 +253,47 @@ describe('Streamer', function () {
});
});
describe
(
'Automatic reconnection'
,
function
()
{
function
delay
(
ms
)
{
return
new
Promise
(
resolve
=>
setTimeout
(
resolve
,
ms
));
}
it
(
'should reconnect when user changes'
,
function
()
{
let
oldWebSocket
;
createDefaultStreamer
();
return
activeStreamer
.
connect
()
.
then
(
function
()
{
oldWebSocket
=
fakeWebSocket
;
fakeStore
.
profile
.
returns
({
userid
:
'somebody'
});
return
fakeStore
.
setState
({});
})
.
then
(
function
()
{
assert
.
ok
(
oldWebSocket
.
didClose
);
assert
.
ok
(
!
fakeWebSocket
.
didClose
);
});
});
it
(
'should only set up auto-reconnect once'
,
async
()
=>
{
createDefaultStreamer
();
// This should register auto-reconnect
await
activeStreamer
.
connect
();
// Call connect again: this should not "re-register" auto-reconnect
await
activeStreamer
.
connect
();
// This should trigger auto-reconnect, but only once, proving that
// only one registration happened
fakeStore
.
profile
.
returns
({
userid
:
'somebody'
});
fakeStore
.
setState
({});
await
delay
(
1
);
// Total number of web sockets blown through in this test should be 2
// 3+ would indicate `reconnect` fired more than once
assert
.
lengthOf
(
fakeWebSockets
,
2
);
});
});
describe
(
'annotation notifications'
,
function
()
{
beforeEach
(
function
()
{
createDefaultStreamer
();
...
...
src/sidebar/store/modules/activity.js
View file @
d0963217
...
...
@@ -19,6 +19,10 @@ function init() {
* The number of annotation fetches that have started and not yet completed.
*/
activeAnnotationFetches
:
0
,
/**
* Have annotations ever been fetched?
*/
hasFetchedAnnotations
:
false
,
};
}
...
...
@@ -86,6 +90,7 @@ const update = {
return
{
...
state
,
hasFetchedAnnotations
:
true
,
activeAnnotationFetches
:
state
.
activeAnnotationFetches
-
1
,
};
},
...
...
@@ -127,6 +132,10 @@ function apiRequestFinished() {
/** Selectors */
function
hasFetchedAnnotations
(
state
)
{
return
state
.
activity
.
hasFetchedAnnotations
;
}
/**
* Return true when annotations are actively being fetched.
*/
...
...
@@ -173,6 +182,7 @@ export default {
},
selectors
:
{
hasFetchedAnnotations
,
isLoading
,
isFetchingAnnotations
,
isSavingAnnotation
,
...
...
src/sidebar/store/modules/direct-linked.js
View file @
d0963217
...
...
@@ -136,6 +136,10 @@ function directLinkedGroupId(state) {
return
state
.
directLinked
.
directLinkedGroupId
;
}
function
directLinkedGroupFetchFailed
(
state
)
{
return
state
.
directLinked
.
directLinkedGroupFetchFailed
;
}
export
default
{
init
,
namespace
:
'directLinked'
,
...
...
@@ -149,6 +153,7 @@ export default {
},
selectors
:
{
directLinkedAnnotationId
,
directLinkedGroupFetchFailed
,
directLinkedGroupId
,
},
};
src/sidebar/store/modules/frames.js
View file @
d0963217
import
{
createSelector
}
from
'reselect'
;
import
{
createSelector
,
createSelectorCreator
,
defaultMemoize
,
}
from
'reselect'
;
import
shallowEqual
from
'shallowequal'
;
import
*
as
util
from
'../util'
;
...
...
@@ -103,15 +108,23 @@ function searchUrisForFrame(frame) {
return
uris
;
}
/**
* Return the set of URIs that should be used to search for annotations on the
* current page.
*/
function
searchUris
(
state
)
{
return
state
.
frames
.
reduce
(
function
(
uris
,
frame
)
{
return
uris
.
concat
(
searchUrisForFrame
(
frame
));
},
[]);
}
// "selector creator" that uses `shallowEqual` instead of `===` for memoization
const
createShallowEqualSelector
=
createSelectorCreator
(
defaultMemoize
,
shallowEqual
);
// Memoized selector will return the same array (of URIs) reference unless the
// values of the array change (are not shallow-equal).
const
searchUris
=
createShallowEqualSelector
(
state
=>
{
return
state
.
frames
.
reduce
(
(
uris
,
frame
)
=>
uris
.
concat
(
searchUrisForFrame
(
frame
)),
[]
);
},
uris
=>
uris
);
export
default
{
init
:
init
,
...
...
src/sidebar/store/modules/selection.js
View file @
d0963217
...
...
@@ -519,6 +519,21 @@ function getSelectedAnnotationMap(state) {
return
state
.
selection
.
selectedAnnotationMap
;
}
/**
* Is any sort of filtering currently applied to the list of annotations? This
* includes a search query, but also if annotations are selected or a user
* is focused.
*
* @return {boolean}
*/
const
hasAppliedFilter
=
createSelector
(
filterQuery
,
focusModeFocused
,
hasSelectedAnnotations
,
(
filterQuery
,
focusModeFocused
,
hasSelectedAnnotations
)
=>
!!
filterQuery
||
focusModeFocused
||
hasSelectedAnnotations
);
export
default
{
init
:
init
,
namespace
:
'selection'
,
...
...
@@ -541,7 +556,6 @@ export default {
},
selectors
:
{
hasSelectedAnnotations
,
expandedThreads
,
filterQuery
,
focusModeFocused
,
...
...
@@ -553,5 +567,7 @@ export default {
isAnnotationSelected
,
getFirstSelectedAnnotationId
,
getSelectedAnnotationMap
,
hasAppliedFilter
,
hasSelectedAnnotations
,
},
};
src/sidebar/store/modules/test/activity-test.js
View file @
d0963217
...
...
@@ -8,6 +8,23 @@ describe('sidebar/store/modules/activity', () => {
store
=
createStore
([
activity
]);
});
describe
(
'hasFetchedAnnotations'
,
()
=>
{
it
(
'returns false if no fetches have completed yet'
,
()
=>
{
assert
.
isFalse
(
store
.
hasFetchedAnnotations
());
});
it
(
'returns false after fetch(es) started'
,
()
=>
{
store
.
annotationFetchStarted
();
assert
.
isFalse
(
store
.
hasFetchedAnnotations
());
});
it
(
'returns true once a fetch has finished'
,
()
=>
{
store
.
annotationFetchStarted
();
store
.
annotationFetchFinished
();
assert
.
isTrue
(
store
.
hasFetchedAnnotations
());
});
});
describe
(
'#isLoading'
,
()
=>
{
it
(
'returns false with the initial state'
,
()
=>
{
assert
.
equal
(
store
.
isLoading
(),
false
);
...
...
src/sidebar/store/modules/test/direct-linked-test.js
View file @
d0963217
...
...
@@ -88,6 +88,13 @@ describe('sidebar/store/modules/direct-linked', () => {
});
});
describe
(
'#directLinkedGroupFetchFailed'
,
()
=>
{
it
(
'should return the group fetch failed status'
,
()
=>
{
store
.
setDirectLinkedGroupFetchFailed
(
true
);
assert
.
isTrue
(
store
.
directLinkedGroupFetchFailed
());
});
});
describe
(
'#directLinkedGroupId'
,
()
=>
{
it
(
'should return the current direct-linked group ID'
,
()
=>
{
store
.
setDirectLinkedGroupId
(
'group-id'
);
...
...
src/sidebar/store/modules/test/frames-test.js
View file @
d0963217
...
...
@@ -157,7 +157,12 @@ describe('sidebar/store/modules/frames', function () {
testCase
.
frames
.
forEach
(
frame
=>
{
store
.
connectFrame
(
frame
);
});
assert
.
deepEqual
(
store
.
searchUris
(),
testCase
.
searchUris
);
const
firstResults
=
store
.
searchUris
();
const
secondResults
=
store
.
searchUris
();
assert
.
deepEqual
(
firstResults
,
testCase
.
searchUris
);
// The selector is memoized and should return the same Array reference
// assuming the list of search URIs hasn't changed
assert
.
equal
(
firstResults
,
secondResults
);
});
});
});
...
...
src/sidebar/store/modules/test/selection-test.js
View file @
d0963217
...
...
@@ -62,6 +62,34 @@ describe('sidebar/store/modules/selection', () => {
});
});
describe
(
'hasAppliedFilter'
,
()
=>
{
it
(
'returns true if there is a search query set'
,
()
=>
{
store
.
setFilterQuery
(
'foobar'
);
assert
.
isTrue
(
store
.
hasAppliedFilter
());
});
it
(
'returns true if in user-focused mode'
,
()
=>
{
store
=
createStore
([
selection
],
[{
focus
:
{
user
:
{}
}
}]);
store
.
setFocusModeFocused
(
true
);
assert
.
isTrue
(
store
.
hasAppliedFilter
());
});
it
(
'returns true if there are selected annotations'
,
()
=>
{
store
.
selectAnnotations
([
1
]);
assert
.
isTrue
(
store
.
hasAppliedFilter
());
});
it
(
'returns false after selection is cleared'
,
()
=>
{
store
.
setFilterQuery
(
'foobar'
);
store
.
clearSelection
();
assert
.
isFalse
(
store
.
hasAppliedFilter
());
});
});
describe
(
'hasSelectedAnnotations'
,
function
()
{
it
(
'returns true if there are any selected annotations'
,
function
()
{
store
.
selectAnnotations
([
1
]);
...
...
src/sidebar/store/modules/test/viewer-test.js
View file @
d0963217
...
...
@@ -19,4 +19,23 @@ describe('store/modules/viewer', function () {
assert
.
isFalse
(
store
.
getState
().
viewer
.
visibleHighlights
);
});
});
describe
(
'hasSidebarOpened'
,
()
=>
{
it
(
'is `false` if sidebar has never been opened'
,
()
=>
{
assert
.
isFalse
(
store
.
hasSidebarOpened
());
store
.
setSidebarOpened
(
false
);
assert
.
isFalse
(
store
.
hasSidebarOpened
());
});
it
(
'is `true` if sidebar has been opened'
,
()
=>
{
store
.
setSidebarOpened
(
true
);
assert
.
isTrue
(
store
.
hasSidebarOpened
());
});
it
(
'is `true` if sidebar is closed after being opened'
,
()
=>
{
store
.
setSidebarOpened
(
true
);
store
.
setSidebarOpened
(
false
);
assert
.
isTrue
(
store
.
hasSidebarOpened
());
});
});
});
src/sidebar/store/modules/viewer.js
View file @
d0963217
...
...
@@ -7,6 +7,9 @@ import * as util from '../util';
function
init
()
{
return
{
// Has the sidebar ever been opened? NB: This is not necessarily the
// current state of the sidebar, but tracks whether it has ever been open
sidebarHasOpened
:
false
,
visibleHighlights
:
false
,
};
}
...
...
@@ -15,10 +18,20 @@ const update = {
SET_HIGHLIGHTS_VISIBLE
:
function
(
state
,
action
)
{
return
{
visibleHighlights
:
action
.
visible
};
},
SET_SIDEBAR_OPENED
:
(
state
,
action
)
=>
{
if
(
action
.
opened
===
true
)
{
// If the sidebar is open, track that it has ever been opened
return
{
sidebarHasOpened
:
true
};
}
// Otherwise, nothing to do here
return
{};
},
};
const
actions
=
util
.
actionTypes
(
update
);
// Action creators
/**
* Sets whether annotation highlights in connected documents are shown
* or not.
...
...
@@ -27,12 +40,28 @@ function setShowHighlights(show) {
return
{
type
:
actions
.
SET_HIGHLIGHTS_VISIBLE
,
visible
:
show
};
}
/**
* @param {boolean} sidebarState - If the sidebar is open
*/
function
setSidebarOpened
(
opened
)
{
return
{
type
:
actions
.
SET_SIDEBAR_OPENED
,
opened
};
}
// Selectors
function
hasSidebarOpened
(
state
)
{
return
state
.
viewer
.
sidebarHasOpened
;
}
export
default
{
init
:
init
,
namespace
:
'viewer'
,
update
:
update
,
actions
:
{
setShowHighlights
:
setShowHighlights
,
setShowHighlights
,
setSidebarOpened
,
},
selectors
:
{
hasSidebarOpened
,
},
selectors
:
{},
};
src/sidebar/templates/hypothesis-app.html
View file @
d0963217
...
...
@@ -15,11 +15,7 @@
<main
ng-if=
"vm.route()"
>
<annotation-viewer-content
ng-if=
"vm.route() == 'annotation'"
></annotation-viewer-content>
<stream-content
ng-if=
"vm.route() == 'stream'"
></stream-content>
<sidebar-content
ng-if=
"vm.route() == 'sidebar'"
auth=
"vm.auth"
on-login=
"vm.login()"
on-sign-up=
"vm.signUp()"
></sidebar-content>
<sidebar-content
ng-if=
"vm.route() == 'sidebar'"
on-login=
"vm.login()"
on-signUp=
"vm.signUp()"
></sidebar-content>
</main>
</div>
</div>
src/sidebar/templates/sidebar-content.html
deleted
100644 → 0
View file @
f2d53d7b
<focused-mode-header
ng-if=
"vm.showFocusedHeader()"
>
</focused-mode-header>
<login-prompt-panel
on-login=
"vm.onLogin()"
on-sign-up=
"vm.onSignUp()"
></login-prompt-panel>
<!-- Display error message if direct-linked annotation fetch failed. -->
<sidebar-content-error
error-type=
"'annotation'"
on-login-request=
"vm.onLogin()"
ng-if=
"vm.selectedAnnotationUnavailable()"
>
</sidebar-content-error>
<!-- Display error message if direct-linked group fetch failed. -->
<sidebar-content-error
error-type=
"'group'"
on-login-request=
"vm.onLogin()"
ng-if=
"vm.selectedGroupUnavailable()"
>
</sidebar-content-error>
<selection-tabs
ng-if=
"vm.showSelectedTabs()"
is-loading=
"vm.isLoading()"
>
</selection-tabs>
<search-status-bar
ng-if=
"!vm.isLoading() && !(vm.selectedAnnotationUnavailable() || vm.selectedGroupUnavailable())"
>
</search-status-bar>
<thread-list
thread=
"vm.rootThread()"
></thread-list>
<logged-out-message
ng-if=
"vm.shouldShowLoggedOutMessage()"
on-login=
"vm.onLogin()"
>
</logged-out-message>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment