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
d50476db
Unverified
Commit
d50476db
authored
Apr 24, 2020
by
Robert Knight
Committed by
GitHub
Apr 24, 2020
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #2078 from hypothesis/convert-stream-content
Convert stream content to Preact
parents
bd7cc1c3
b742694a
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
152 additions
and
143 deletions
+152
-143
stream-content.js
src/sidebar/components/stream-content.js
+71
-52
stream-content-test.js
src/sidebar/components/test/stream-content-test.js
+79
-84
index.js
src/sidebar/index.js
+2
-2
stream-content.html
src/sidebar/templates/stream-content.html
+0
-5
No files found.
src/sidebar/components/stream-content.js
View file @
d50476db
import
{
watch
}
from
'../util/watch'
;
import
{
createElement
}
from
'preact'
;
import
{
useCallback
,
useEffect
}
from
'preact/hooks'
;
import
propTypes
from
'prop-types'
;
// @ngInject
import
{
withServices
}
from
'../util/service-context'
;
function
StreamContentController
(
$scope
,
store
,
api
,
rootThread
,
searchFilter
)
{
import
useStore
from
'../store/use-store'
;
/** `offset` parameter for the next search API call. */
let
offset
=
0
;
/** Load annotations fetched from the API into the app. */
import
ThreadList
from
'./thread-list'
;
const
load
=
function
(
result
)
{
offset
+=
result
.
rows
.
length
;
const
annots
=
[...
result
.
rows
,
...
result
.
replies
];
store
.
addAnnotations
(
annots
);
};
const
currentQuery
=
()
=>
store
.
routeParams
().
q
;
/**
* The main content of the "stream" route (https://hypothes.is/stream)
*/
function
StreamContent
({
api
,
rootThread
:
rootThreadService
,
searchFilter
,
toastMessenger
,
})
{
const
addAnnotations
=
useStore
(
store
=>
store
.
addAnnotations
);
const
clearAnnotations
=
useStore
(
store
=>
store
.
clearAnnotations
);
const
currentQuery
=
useStore
(
store
=>
store
.
routeParams
().
q
);
const
setSortKey
=
useStore
(
store
=>
store
.
setSortKey
);
/**
/**
* Fetch the next `limit` annotations starting from `offset` from the API.
* Fetch annotations from the API and display them in the stream.
*
* @param {string} query - The user-supplied search query
*/
*/
const
fetch
=
function
(
limit
)
{
const
loadAnnotations
=
useCallback
(
const
query
=
Object
.
assign
(
async
query
=>
{
{
const
queryParams
=
{
_separate_replies
:
true
,
_separate_replies
:
true
,
offset
:
offset
,
limit
:
limit
,
},
searchFilter
.
toObject
(
currentQuery
())
);
api
// nb. There is currently no way to load anything except the first
.
search
(
query
)
// 20 matching annotations in the UI.
.
then
(
load
)
offset
:
0
,
.
catch
(
function
(
err
)
{
limit
:
20
,
console
.
error
(
err
);
});
};
function
clearAndFetch
()
{
...
searchFilter
.
toObject
(
query
),
// In case this route loaded after a client-side route change (eg. from
};
// '/a/:id'), clear any existing annotations.
const
results
=
await
api
.
search
(
queryParams
);
store
.
clearAnnotations
();
addAnnotations
([...
results
.
rows
,
...
results
.
replies
]);
},
[
addAnnotations
,
api
,
searchFilter
]
);
// Fetch initial batch of annotations.
// Update the stream when this route is initially displayed and whenever
offset
=
0
;
// the search query is updated.
fetch
(
20
);
useEffect
(()
=>
{
}
// Sort the stream so that the newest annotations are at the top
setSortKey
(
'Newest'
);
clearAnnotations
();
loadAnnotations
(
currentQuery
).
catch
(
err
=>
{
toastMessenger
.
error
(
`Unable to fetch annotations:
${
err
.
message
}
`
);
});
},
[
clearAnnotations
,
currentQuery
,
loadAnnotations
,
setSortKey
,
toastMessenger
,
]);
const
unsubscribe
=
watch
(
store
.
subscribe
,
currentQuery
,
()
=>
{
const
rootThread
=
useStore
(
store
=>
clearAndFetch
();
rootThreadService
.
thread
(
store
.
getState
())
});
);
$scope
.
$on
(
'$destroy'
,
unsubscribe
);
clearAndFetch
();
return
<
ThreadList
thread
=
{
rootThread
}
/>
;
this
.
setCollapsed
=
store
.
setCollapsed
;
this
.
rootThread
=
()
=>
rootThread
.
thread
(
store
.
getState
());
// Sort the stream so that the newest annotations are at the top
store
.
setSortKey
(
'Newest'
);
}
}
export
default
{
StreamContent
.
propTypes
=
{
controller
:
StreamContentController
,
// Injected services.
controllerAs
:
'vm'
,
api
:
propTypes
.
object
,
bindings
:
{},
rootThread
:
propTypes
.
object
,
template
:
require
(
'../templates/stream-content.html'
),
searchFilter
:
propTypes
.
object
,
toastMessenger
:
propTypes
.
object
,
};
};
StreamContent
.
injectedProps
=
[
'api'
,
'rootThread'
,
'searchFilter'
,
'toastMessenger'
,
];
export
default
withServices
(
StreamContent
);
src/sidebar/components/test/stream-content-test.js
View file @
d50476db
import
angular
from
'angular
'
;
import
{
mount
}
from
'enzyme
'
;
import
EventEmitter
from
'tiny-emitter
'
;
import
{
createElement
}
from
'preact
'
;
import
streamContent
from
'../stream-content'
;
import
mockImportedComponents
from
'../../../test-util/mock-imported-components'
;
import
{
waitFor
}
from
'../../../test-util/wait'
;
class
FakeRootThread
extends
EventEmitter
{
import
StreamContent
,
{
$imports
}
from
'../stream-content'
;
constructor
()
{
super
();
this
.
thread
=
sinon
.
stub
();
}
}
describe
(
'StreamContentController'
,
function
()
{
describe
(
'StreamContent'
,
()
=>
{
let
$componentController
;
let
fakeApi
;
let
fakeStore
;
let
fakeRootThread
;
let
fakeRootThread
;
let
fakeSearchFilter
;
let
fakeSearchFilter
;
let
fakeApi
;
let
fakeStore
;
let
fakeStreamer
;
let
fakeToastMessenger
;
let
fakeStreamFilter
;
before
(
function
()
{
beforeEach
(()
=>
{
angular
.
module
(
'h'
,
[]).
component
(
'streamContent'
,
streamContent
);
fakeApi
=
{
});
search
:
sinon
.
stub
().
resolves
({
rows
:
[],
replies
:
[],
total
:
0
}),
};
beforeEach
(
function
()
{
fakeRootThread
=
{
fakeStore
=
{
thread
:
sinon
.
stub
().
returns
({}),
addAnnotations
:
sinon
.
stub
(),
clearAnnotations
:
sinon
.
spy
(),
routeParams
:
sinon
.
stub
().
returns
({
id
:
'test'
}),
setCollapsed
:
sinon
.
spy
(),
setForceVisible
:
sinon
.
spy
(),
setSortKey
:
sinon
.
spy
(),
subscribe
:
sinon
.
spy
(),
};
};
fakeSearchFilter
=
{
fakeSearchFilter
=
{
generateFacetedFilter
:
sinon
.
stub
(),
toObject
:
sinon
.
stub
().
returns
({}),
toObject
:
sinon
.
stub
().
returns
({}),
};
};
fakeApi
=
{
fakeStore
=
{
search
:
sinon
.
stub
().
resolves
({
rows
:
[],
replies
:
[],
total
:
0
}),
addAnnotations
:
sinon
.
stub
(),
};
clearAnnotations
:
sinon
.
spy
(),
getState
:
sinon
.
stub
().
returns
({}),
fakeStreamer
=
{
routeParams
:
sinon
.
stub
().
returns
({
id
:
'test'
}),
open
:
sinon
.
spy
(),
setSortKey
:
sinon
.
spy
(),
close
:
sinon
.
spy
(),
setConfig
:
sinon
.
spy
(),
connect
:
sinon
.
spy
(),
};
};
fakeStreamFilter
=
{
fakeToastMessenger
=
{
resetFilter
:
sinon
.
stub
().
returnsThis
(),
error
:
sinon
.
stub
(),
setMatchPolicyIncludeAll
:
sinon
.
stub
().
returnsThis
(),
getFilter
:
sinon
.
stub
(),
};
};
fakeRootThread
=
new
FakeRootThread
();
$imports
.
$mock
(
mockImportedComponents
());
$imports
.
$mock
({
angular
.
mock
.
module
(
'h'
,
{
'../store/use-store'
:
callback
=>
callback
(
fakeStore
),
store
:
fakeStore
,
api
:
fakeApi
,
rootThread
:
fakeRootThread
,
searchFilter
:
fakeSearchFilter
,
streamFilter
:
fakeStreamFilter
,
streamer
:
fakeStreamer
,
});
});
});
angular
.
mock
.
inject
(
function
(
_$componentController_
)
{
afterEach
(()
=>
{
$componentController
=
_$componentController_
;
$imports
.
$restore
();
});
});
});
function
createController
()
{
function
createComponent
()
{
return
$componentController
(
'streamContent'
,
{},
{});
return
mount
(
<
StreamContent
api
=
{
fakeApi
}
rootThread
=
{
fakeRootThread
}
searchFilter
=
{
fakeSearchFilter
}
toastMessenger
=
{
fakeToastMessenger
}
/
>
);
}
}
it
(
'clears any existing annotations when the /stream route is loaded'
,
()
=>
{
it
(
'clears any existing annotations when the /stream route is loaded'
,
()
=>
{
createCo
ntroller
();
createCo
mponent
();
assert
.
calledOnce
(
fakeStore
.
clearAnnotations
);
assert
.
calledOnce
(
fakeStore
.
clearAnnotations
);
});
});
it
(
'calls the search API with `_separate_replies: true`'
,
function
()
{
it
(
'calls the search API with `_separate_replies: true`'
,
()
=>
{
createCo
ntroller
();
createCo
mponent
();
assert
.
equal
(
fakeApi
.
search
.
firstCall
.
args
[
0
].
_separate_replies
,
true
);
assert
.
equal
(
fakeApi
.
search
.
firstCall
.
args
[
0
].
_separate_replies
,
true
);
});
});
it
(
'passes the annotations and replies from search to loadAnnotations()'
,
function
()
{
it
(
'loads the annotations and replies into the store'
,
async
()
=>
{
fakeApi
.
search
=
function
()
{
fakeApi
.
search
.
resolves
({
return
Promise
.
resolve
({
rows
:
[
'annotation_1'
,
'annotation_2'
],
rows
:
[
'annotation_1'
,
'annotation_2'
],
replies
:
[
'reply_1'
,
'reply_2'
,
'reply_3'
],
replies
:
[
'reply_1'
,
'reply_2'
,
'reply_3'
],
});
};
createController
();
return
Promise
.
resolve
().
then
(
function
()
{
assert
.
calledOnce
(
fakeStore
.
addAnnotations
);
assert
.
calledWith
(
fakeStore
.
addAnnotations
,
[
'annotation_1'
,
'annotation_2'
,
'reply_1'
,
'reply_2'
,
'reply_3'
,
]);
});
});
createComponent
();
await
waitFor
(()
=>
fakeStore
.
addAnnotations
.
called
);
assert
.
calledOnce
(
fakeStore
.
addAnnotations
);
assert
.
calledWith
(
fakeStore
.
addAnnotations
,
[
'annotation_1'
,
'annotation_2'
,
'reply_1'
,
'reply_2'
,
'reply_3'
,
]);
});
it
(
'displays an error if fetching annotations fails'
,
async
()
=>
{
fakeApi
.
search
.
rejects
(
new
Error
(
'Server error'
));
createComponent
();
await
waitFor
(()
=>
fakeToastMessenger
.
error
.
called
);
assert
.
calledWith
(
fakeToastMessenger
.
error
,
'Unable to fetch annotations: Server error'
);
});
});
context
(
'when route parameters change'
,
function
()
{
context
(
'when route parameters change'
,
()
=>
{
it
(
'updates annotations if the query changed'
,
function
()
{
it
(
'updates annotations if the query changed'
,
()
=>
{
fakeStore
.
routeParams
.
returns
({
q
:
'test query'
});
fakeStore
.
routeParams
.
returns
({
q
:
'test query'
});
c
reateController
();
c
onst
wrapper
=
createComponent
();
fakeStore
.
clearAnnotations
.
resetHistory
();
fakeStore
.
clearAnnotations
.
resetHistory
();
fakeApi
.
search
.
resetHistory
();
fakeApi
.
search
.
resetHistory
();
fakeStore
.
routeParams
.
returns
({
q
:
'new query'
});
fakeStore
.
routeParams
.
returns
({
q
:
'new query'
});
fakeStore
.
subscribe
.
lastCall
.
callback
();
// Force update. `useStore` handles this in the real app.
wrapper
.
setProps
({});
assert
.
called
(
fakeStore
.
clearAnnotations
);
assert
.
called
(
fakeStore
.
clearAnnotations
);
assert
.
called
(
fakeApi
.
search
);
assert
.
called
(
fakeApi
.
search
);
});
});
it
(
'does not clear annotations if the query did not change'
,
function
()
{
it
(
'does not clear annotations if the query did not change'
,
()
=>
{
fakeStore
.
routeParams
.
returns
({
q
:
'test query'
});
fakeStore
.
routeParams
.
returns
({
q
:
'test query'
});
c
reateController
();
c
onst
wrapper
=
createComponent
();
fakeApi
.
search
.
resetHistory
();
fakeApi
.
search
.
resetHistory
();
fakeStore
.
clearAnnotations
.
resetHistory
();
fakeStore
.
clearAnnotations
.
resetHistory
();
fakeStore
.
subscribe
.
lastCall
.
callback
();
fakeStore
.
routeParams
.
returns
({
q
:
'test query'
,
other_param
:
'foo'
});
// Force update. `useStore` handles this in the real app.
wrapper
.
setProps
({});
assert
.
notCalled
(
fakeStore
.
clearAnnotations
);
assert
.
notCalled
(
fakeStore
.
clearAnnotations
);
assert
.
notCalled
(
fakeApi
.
search
);
assert
.
notCalled
(
fakeApi
.
search
);
...
...
src/sidebar/index.js
View file @
d50476db
...
@@ -129,6 +129,7 @@ import SearchStatusBar from './components/search-status-bar';
...
@@ -129,6 +129,7 @@ import SearchStatusBar from './components/search-status-bar';
import
SelectionTabs
from
'./components/selection-tabs'
;
import
SelectionTabs
from
'./components/selection-tabs'
;
import
ShareAnnotationsPanel
from
'./components/share-annotations-panel'
;
import
ShareAnnotationsPanel
from
'./components/share-annotations-panel'
;
import
SidebarContentError
from
'./components/sidebar-content-error'
;
import
SidebarContentError
from
'./components/sidebar-content-error'
;
import
StreamContent
from
'./components/stream-content'
;
import
SvgIcon
from
'../shared/components/svg-icon'
;
import
SvgIcon
from
'../shared/components/svg-icon'
;
import
Thread
from
'./components/thread'
;
import
Thread
from
'./components/thread'
;
import
ThreadList
from
'./components/thread-list'
;
import
ThreadList
from
'./components/thread-list'
;
...
@@ -139,7 +140,6 @@ import TopBar from './components/top-bar';
...
@@ -139,7 +140,6 @@ import TopBar from './components/top-bar';
import
hypothesisApp
from
'./components/hypothesis-app'
;
import
hypothesisApp
from
'./components/hypothesis-app'
;
import
sidebarContent
from
'./components/sidebar-content'
;
import
sidebarContent
from
'./components/sidebar-content'
;
import
streamContent
from
'./components/stream-content'
;
// Services.
// Services.
...
@@ -271,7 +271,7 @@ function startAngularApp(config) {
...
@@ -271,7 +271,7 @@ function startAngularApp(config) {
.
component
(
'sidebarContent'
,
sidebarContent
)
.
component
(
'sidebarContent'
,
sidebarContent
)
.
component
(
'sidebarContentError'
,
wrapComponent
(
SidebarContentError
))
.
component
(
'sidebarContentError'
,
wrapComponent
(
SidebarContentError
))
.
component
(
'shareAnnotationsPanel'
,
wrapComponent
(
ShareAnnotationsPanel
))
.
component
(
'shareAnnotationsPanel'
,
wrapComponent
(
ShareAnnotationsPanel
))
.
component
(
'streamContent'
,
streamContent
)
.
component
(
'streamContent'
,
wrapComponent
(
StreamContent
)
)
.
component
(
'svgIcon'
,
wrapComponent
(
SvgIcon
))
.
component
(
'svgIcon'
,
wrapComponent
(
SvgIcon
))
.
component
(
'thread'
,
wrapComponent
(
Thread
))
.
component
(
'thread'
,
wrapComponent
(
Thread
))
.
component
(
'threadList'
,
wrapComponent
(
ThreadList
))
.
component
(
'threadList'
,
wrapComponent
(
ThreadList
))
...
...
src/sidebar/templates/stream-content.html
deleted
100644 → 0
View file @
bd7cc1c3
<thread-list
on-change-collapsed=
"vm.setCollapsed(id, collapsed)"
show-document-info=
"true"
thread=
"vm.rootThread()"
>
</thread-list>
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