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
0a894ccc
Commit
0a894ccc
authored
Jun 02, 2016
by
Robert Knight
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #3379 from hypothesis/sort-state-redux
Move canonical annotation list sort state to Redux store
parents
b082181f
708f2847
Changes
15
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
57 additions
and
122 deletions
+57
-122
annotation-ui.js
h/static/scripts/annotation-ui.js
+11
-8
app-controller.js
h/static/scripts/app-controller.js
+8
-30
sort-dropdown.js
h/static/scripts/directive/sort-dropdown.js
+9
-11
sort-dropdown-test.js
h/static/scripts/directive/test/sort-dropdown-test.js
+5
-5
top-bar.js
h/static/scripts/directive/top-bar.js
+3
-3
root-thread.js
h/static/scripts/root-thread.js
+1
-1
stream-controller.coffee
h/static/scripts/stream-controller.coffee
+3
-6
app-controller-test.js
h/static/scripts/test/app-controller-test.js
+0
-21
threading-test.js
h/static/scripts/test/integration/threading-test.js
+3
-3
root-thread-test.js
h/static/scripts/test/root-thread-test.js
+4
-2
stream-controller-test.coffee
h/static/scripts/test/stream-controller-test.coffee
+1
-1
widget-controller.js
h/static/scripts/widget-controller.js
+0
-5
sort_dropdown.html
h/templates/client/sort_dropdown.html
+6
-15
top_bar.html
h/templates/client/top_bar.html
+3
-4
viewer.html
h/templates/client/viewer.html
+0
-7
No files found.
h/static/scripts/annotation-ui.js
View file @
0a894ccc
...
@@ -52,7 +52,10 @@ function initialState(settings) {
...
@@ -52,7 +52,10 @@ function initialState(settings) {
filterQuery
:
null
,
filterQuery
:
null
,
sortMode
:
'Location'
,
// Key by which annotations are currently sorted.
sortKey
:
'Location'
,
// Keys by which annotations can be sorted.
sortKeysAvailable
:
[
'Newest'
,
'Oldest'
,
'Location'
],
});
});
}
}
...
@@ -66,7 +69,7 @@ var types = {
...
@@ -66,7 +69,7 @@ var types = {
REMOVE_ANNOTATIONS
:
'REMOVE_ANNOTATIONS'
,
REMOVE_ANNOTATIONS
:
'REMOVE_ANNOTATIONS'
,
CLEAR_ANNOTATIONS
:
'CLEAR_ANNOTATIONS'
,
CLEAR_ANNOTATIONS
:
'CLEAR_ANNOTATIONS'
,
SET_FILTER_QUERY
:
'SET_FILTER_QUERY'
,
SET_FILTER_QUERY
:
'SET_FILTER_QUERY'
,
S
ORT_BY
:
'SORT_B
Y'
,
S
ET_SORT_KEY
:
'SET_SORT_KE
Y'
,
};
};
function
excludeAnnotations
(
current
,
annotations
)
{
function
excludeAnnotations
(
current
,
annotations
)
{
...
@@ -116,8 +119,8 @@ function reducer(state, action) {
...
@@ -116,8 +119,8 @@ function reducer(state, action) {
forceVisible
:
{},
forceVisible
:
{},
expanded
:
{},
expanded
:
{},
});
});
case
types
.
S
ORT_B
Y
:
case
types
.
S
ET_SORT_KE
Y
:
return
Object
.
assign
({},
state
,
{
sort
Mode
:
action
.
mode
});
return
Object
.
assign
({},
state
,
{
sort
Key
:
action
.
key
});
default
:
default
:
return
state
;
return
state
;
}
}
...
@@ -301,11 +304,11 @@ module.exports = function (settings) {
...
@@ -301,11 +304,11 @@ module.exports = function (settings) {
});
});
},
},
/** Sets the sort
mode
for the annotation list. */
/** Sets the sort
key
for the annotation list. */
s
ortBy
:
function
(
mode
)
{
s
etSortKey
:
function
(
key
)
{
store
.
dispatch
({
store
.
dispatch
({
type
:
types
.
S
ORT_B
Y
,
type
:
types
.
S
ET_SORT_KE
Y
,
mode
:
mode
,
key
:
key
,
});
});
},
},
};
};
...
...
h/static/scripts/app-controller.js
View file @
0a894ccc
...
@@ -3,7 +3,6 @@
...
@@ -3,7 +3,6 @@
var
angular
=
require
(
'angular'
);
var
angular
=
require
(
'angular'
);
var
scrollIntoView
=
require
(
'scroll-into-view'
);
var
scrollIntoView
=
require
(
'scroll-into-view'
);
var
annotationMetadata
=
require
(
'./annotation-metadata'
);
var
events
=
require
(
'./events'
);
var
events
=
require
(
'./events'
);
var
parseAccountID
=
require
(
'./filter/persona'
).
parseAccountID
;
var
parseAccountID
=
require
(
'./filter/persona'
).
parseAccountID
;
var
scopeTimeout
=
require
(
'./util/scope-timeout'
);
var
scopeTimeout
=
require
(
'./util/scope-timeout'
);
...
@@ -53,12 +52,16 @@ module.exports = function AppController(
...
@@ -53,12 +52,16 @@ module.exports = function AppController(
// the stream page or an individual annotation page.
// the stream page or an individual annotation page.
$scope
.
isSidebar
=
$window
.
top
!==
$window
;
$scope
.
isSidebar
=
$window
.
top
!==
$window
;
// Default sort
$scope
.
sortKey
=
function
()
{
$scope
.
sort
=
{
return
annotationUI
.
getState
().
sortKey
;
name
:
'Location'
,
options
:
[
'Newest'
,
'Oldest'
,
'Location'
]
};
};
$scope
.
sortKeysAvailable
=
function
()
{
return
annotationUI
.
getState
().
sortKeysAvailable
;
};
$scope
.
setSortKey
=
annotationUI
.
setSortKey
;
// Reload the view when the user switches accounts
// Reload the view when the user switches accounts
$scope
.
$on
(
events
.
USER_CHANGED
,
function
(
event
,
data
)
{
$scope
.
$on
(
events
.
USER_CHANGED
,
function
(
event
,
data
)
{
$scope
.
auth
=
authStateFromUserID
(
data
.
userid
);
$scope
.
auth
=
authStateFromUserID
(
data
.
userid
);
...
@@ -79,31 +82,6 @@ module.exports = function AppController(
...
@@ -79,31 +82,6 @@ module.exports = function AppController(
}
}
});
});
$scope
.
$watch
(
'sort.name'
,
function
(
name
)
{
if
(
!
name
)
{
return
;
}
var
predicateFn
;
switch
(
name
)
{
case
'Newest'
:
predicateFn
=
[
'-!!message'
,
'-message.updated'
];
break
;
case
'Oldest'
:
predicateFn
=
[
'-!!message'
,
'message.updated'
];
break
;
case
'Location'
:
predicateFn
=
function
(
thread
)
{
return
annotationMetadata
.
location
(
thread
.
message
);
};
break
;
}
$scope
.
sort
=
{
name
:
name
,
predicate
:
predicateFn
,
options
:
$scope
.
sort
.
options
,
};
});
/** Scroll to the view to the element matching the given selector */
/** Scroll to the view to the element matching the given selector */
function
scrollToView
(
selector
)
{
function
scrollToView
(
selector
)
{
// Add a timeout so that if the element has just been shown (eg. via ngIf)
// Add a timeout so that if the element has just been shown (eg. via ngIf)
...
...
h/static/scripts/directive/sort-dropdown.js
View file @
0a894ccc
'use strict'
;
module
.
exports
=
function
()
{
module
.
exports
=
function
()
{
return
{
return
{
restrict
:
'E'
,
restrict
:
'E'
,
scope
:
{
scope
:
{
/** The name of the currently selected sort criteria. */
/** The name of the currently selected sort key. */
sortBy
:
'<'
,
sortKey
:
'<'
,
/** A list of choices that the user can opt to sort by. */
/** A list of possible keys that the user can opt to sort by. */
sortOptions
:
'<'
,
sortKeysAvailable
:
'<'
,
/** If true, the menu uses just an icon, otherwise
/** Called when the user changes the sort key. */
* it displays 'Sorted by {{sortBy}}'
onChangeSortKey
:
'&'
,
*/
showAsIcon
:
'<'
,
/** Called when the user changes the current sort criteria. */
onChangeSortBy
:
'&'
,
},
},
template
:
require
(
'../../../templates/client/sort_dropdown.html'
),
template
:
require
(
'../../../templates/client/sort_dropdown.html'
),
}
}
}
}
;
h/static/scripts/directive/test/sort-dropdown-test.js
View file @
0a894ccc
...
@@ -14,13 +14,13 @@ describe('sortDropdown', function () {
...
@@ -14,13 +14,13 @@ describe('sortDropdown', function () {
angular
.
mock
.
module
(
'app'
);
angular
.
mock
.
module
(
'app'
);
});
});
it
(
'should update the sort
mode
on click'
,
function
()
{
it
(
'should update the sort
key
on click'
,
function
()
{
var
changeSpy
=
sinon
.
spy
();
var
changeSpy
=
sinon
.
spy
();
var
elem
=
util
.
createDirective
(
document
,
'sortDropdown'
,
{
var
elem
=
util
.
createDirective
(
document
,
'sortDropdown'
,
{
sort
Options
:
[
'Newest'
,
'Oldest'
],
sort
KeysAvailable
:
[
'Newest'
,
'Oldest'
],
sort
B
y
:
'Newest'
,
sort
Ke
y
:
'Newest'
,
onChangeSort
B
y
:
{
onChangeSort
Ke
y
:
{
args
:
[
'sort
B
y'
],
args
:
[
'sort
Ke
y'
],
callback
:
changeSpy
,
callback
:
changeSpy
,
}
}
});
});
...
...
h/static/scripts/directive/top-bar.js
View file @
0a894ccc
...
@@ -13,9 +13,9 @@ module.exports = function () {
...
@@ -13,9 +13,9 @@ module.exports = function () {
searchController
:
'<'
,
searchController
:
'<'
,
accountDialog
:
'<'
,
accountDialog
:
'<'
,
shareDialog
:
'<'
,
shareDialog
:
'<'
,
sort
B
y
:
'<'
,
sort
Ke
y
:
'<'
,
sort
Options
:
'<'
,
sort
KeysAvailable
:
'<'
,
onChangeSort
B
y
:
'&'
,
onChangeSort
Ke
y
:
'&'
,
},
},
template
:
require
(
'../../../templates/client/top_bar.html'
),
template
:
require
(
'../../../templates/client/top_bar.html'
),
};
};
...
...
h/static/scripts/root-thread.js
View file @
0a894ccc
...
@@ -50,7 +50,7 @@ function RootThread($rootScope, annotationUI, searchFilter, viewFilter) {
...
@@ -50,7 +50,7 @@ function RootThread($rootScope, annotationUI, searchFilter, viewFilter) {
* settings change.
* settings change.
*/
*/
function
rebuildRootThread
()
{
function
rebuildRootThread
()
{
var
sortFn
=
sortFns
[
annotationUI
.
getState
().
sort
Mode
];
var
sortFn
=
sortFns
[
annotationUI
.
getState
().
sort
Key
];
var
filters
;
var
filters
;
var
filterQuery
=
annotationUI
.
getState
().
filterQuery
;
var
filterQuery
=
annotationUI
.
getState
().
filterQuery
;
...
...
h/static/scripts/stream-controller.coffee
View file @
0a894ccc
...
@@ -46,10 +46,6 @@ module.exports = class StreamController
...
@@ -46,10 +46,6 @@ module.exports = class StreamController
# Perform the initial search
# Perform the initial search
fetch
(
20
)
fetch
(
20
)
$scope
.
$watch
(
'sort.name'
,
(
name
)
->
annotationUI
.
sortBy
(
name
)
)
$scope
.
setCollapsed
=
(
id
,
collapsed
)
->
$scope
.
setCollapsed
=
(
id
,
collapsed
)
->
annotationUI
.
setCollapsed
(
id
,
collapsed
)
annotationUI
.
setCollapsed
(
id
,
collapsed
)
...
@@ -64,9 +60,10 @@ module.exports = class StreamController
...
@@ -64,9 +60,10 @@ module.exports = class StreamController
};
};
);
);
# Sort the stream so that the newest annotations are at the top
annotationUI
.
setSortKey
(
'Newest'
)
$scope
.
isStream
=
true
$scope
.
isStream
=
true
$scope
.
sortOptions
=
[
'Newest'
,
'Oldest'
]
$scope
.
sort
.
name
=
'Newest'
$scope
.
rootThread
=
->
$scope
.
rootThread
=
->
return
rootThread
.
thread
()
return
rootThread
.
thread
()
$scope
.
loadMore
=
fetch
$scope
.
loadMore
=
fetch
h/static/scripts/test/app-controller-test.js
View file @
0a894ccc
...
@@ -3,7 +3,6 @@
...
@@ -3,7 +3,6 @@
var
angular
=
require
(
'angular'
);
var
angular
=
require
(
'angular'
);
var
proxyquire
=
require
(
'proxyquire'
);
var
proxyquire
=
require
(
'proxyquire'
);
var
annotationFixtures
=
require
(
'./annotation-fixtures'
);
var
events
=
require
(
'../events'
);
var
events
=
require
(
'../events'
);
var
util
=
require
(
'./util'
);
var
util
=
require
(
'./util'
);
...
@@ -292,24 +291,4 @@ describe('AppController', function () {
...
@@ -292,24 +291,4 @@ describe('AppController', function () {
assert
.
equal
(
fakeWindow
.
confirm
.
callCount
,
0
);
assert
.
equal
(
fakeWindow
.
confirm
.
callCount
,
0
);
});
});
});
});
describe
(
'sorting'
,
function
()
{
function
annotationThread
()
{
return
{
message
:
annotationFixtures
.
defaultAnnotation
()};
}
it
(
'sorts threads by location when sort name is "Location"'
,
function
()
{
var
threads
=
[
annotationThread
(),
annotationThread
()];
fakeAnnotationMetadata
.
location
=
function
(
annotation
)
{
return
threads
.
findIndex
(
function
(
thread
)
{
return
thread
.
message
===
annotation
;
});
};
createController
();
$scope
.
sort
.
name
=
'Location'
;
$scope
.
$digest
();
assert
.
equal
(
$scope
.
sort
.
predicate
(
threads
[
0
]),
0
);
assert
.
equal
(
$scope
.
sort
.
predicate
(
threads
[
1
]),
1
);
});
});
});
});
h/static/scripts/test/integration/threading-test.js
View file @
0a894ccc
...
@@ -70,16 +70,16 @@ describe('annotation threading', function () {
...
@@ -70,16 +70,16 @@ describe('annotation threading', function () {
unroll
(
'should sort annotations by #mode'
,
function
(
testCase
)
{
unroll
(
'should sort annotations by #mode'
,
function
(
testCase
)
{
annotationUI
.
addAnnotations
(
fixtures
.
annotations
);
annotationUI
.
addAnnotations
(
fixtures
.
annotations
);
annotationUI
.
s
ortBy
(
testCase
.
mode
);
annotationUI
.
s
etSortKey
(
testCase
.
sortKey
);
var
actualOrder
=
rootThread
.
thread
().
children
.
map
(
function
(
thread
)
{
var
actualOrder
=
rootThread
.
thread
().
children
.
map
(
function
(
thread
)
{
return
thread
.
annotation
.
id
;
return
thread
.
annotation
.
id
;
});
});
assert
.
deepEqual
(
actualOrder
,
testCase
.
expectedOrder
);
assert
.
deepEqual
(
actualOrder
,
testCase
.
expectedOrder
);
},
[{
},
[{
mode
:
'Oldest'
,
sortKey
:
'Oldest'
,
expectedOrder
:
[
'1'
,
'2'
],
expectedOrder
:
[
'1'
,
'2'
],
},{
},{
mode
:
'Newest'
,
sortKey
:
'Newest'
,
expectedOrder
:
[
'2'
,
'1'
],
expectedOrder
:
[
'2'
,
'1'
],
}]);
}]);
});
});
h/static/scripts/test/root-thread-test.js
View file @
0a894ccc
...
@@ -37,7 +37,8 @@ describe('rootThread', function () {
...
@@ -37,7 +37,8 @@ describe('rootThread', function () {
expanded
:
{},
expanded
:
{},
forceVisible
:
{},
forceVisible
:
{},
filterQuery
:
null
,
filterQuery
:
null
,
sortMode
:
'Location'
,
sortKey
:
'Location'
,
sortKeysAvailable
:
[
'Location'
],
},
},
getState
:
function
()
{
getState
:
function
()
{
...
@@ -177,7 +178,8 @@ describe('rootThread', function () {
...
@@ -177,7 +178,8 @@ describe('rootThread', function () {
fakeBuildThread
.
reset
();
fakeBuildThread
.
reset
();
fakeAnnotationUI
.
state
=
Object
.
assign
({},
fakeAnnotationUI
.
state
,
{
fakeAnnotationUI
.
state
=
Object
.
assign
({},
fakeAnnotationUI
.
state
,
{
sortMode
:
testCase
.
order
,
sortKey
:
testCase
.
order
,
sortKeysAvailable
:
[
testCase
.
order
],
});
});
rootThread
.
rebuild
();
rootThread
.
rebuild
();
var
sortCompareFn
=
fakeBuildThread
.
args
[
0
][
1
].
sortCompareFn
;
var
sortCompareFn
=
fakeBuildThread
.
args
[
0
][
1
].
sortCompareFn
;
...
...
h/static/scripts/test/stream-controller-test.coffee
View file @
0a894ccc
...
@@ -42,6 +42,7 @@ describe 'StreamController', ->
...
@@ -42,6 +42,7 @@ describe 'StreamController', ->
clearAnnotations
:
sandbox
.
spy
()
clearAnnotations
:
sandbox
.
spy
()
setCollapsed
:
sandbox
.
spy
()
setCollapsed
:
sandbox
.
spy
()
setForceVisible
:
sandbox
.
spy
()
setForceVisible
:
sandbox
.
spy
()
setSortKey
:
sandbox
.
spy
()
}
}
fakeParams
=
{
id
:
'test'
}
fakeParams
=
{
id
:
'test'
}
...
@@ -94,7 +95,6 @@ describe 'StreamController', ->
...
@@ -94,7 +95,6 @@ describe 'StreamController', ->
beforeEach
inject
(
_$controller_
,
$rootScope
)
->
beforeEach
inject
(
_$controller_
,
$rootScope
)
->
$controller
=
_$controller_
$controller
=
_$controller_
$scope
=
$rootScope
.
$new
()
$scope
=
$rootScope
.
$new
()
$scope
.
sort
=
{}
afterEach
->
afterEach
->
sandbox
.
restore
()
sandbox
.
restore
()
...
...
h/static/scripts/widget-controller.js
View file @
0a894ccc
...
@@ -90,8 +90,6 @@ module.exports = function WidgetController(
...
@@ -90,8 +90,6 @@ module.exports = function WidgetController(
visibleThreads
.
detach
();
visibleThreads
.
detach
();
});
});
$scope
.
sortOptions
=
[
'Newest'
,
'Oldest'
,
'Location'
];
function
annotationExists
(
id
)
{
function
annotationExists
(
id
)
{
return
annotationUI
.
getState
().
annotations
.
some
(
function
(
annot
)
{
return
annotationUI
.
getState
().
annotations
.
some
(
function
(
annot
)
{
return
annot
.
id
===
id
;
return
annot
.
id
===
id
;
...
@@ -262,9 +260,6 @@ module.exports = function WidgetController(
...
@@ -262,9 +260,6 @@ module.exports = function WidgetController(
// Watch the inputs that determine which annotations are currently
// Watch the inputs that determine which annotations are currently
// visible and how they are sorted and rebuild the thread when they change
// visible and how they are sorted and rebuild the thread when they change
$scope
.
$watch
(
'sort.name'
,
function
(
mode
)
{
annotationUI
.
sortBy
(
mode
);
});
$scope
.
$watch
(
'search.query'
,
function
(
query
)
{
$scope
.
$watch
(
'search.query'
,
function
(
query
)
{
annotationUI
.
setFilterQuery
(
query
);
annotationUI
.
setFilterQuery
(
query
);
});
});
...
...
h/templates/client/sort_dropdown.html
View file @
0a894ccc
<span
class=
"ng-cloak"
dropdown
keyboard-nav
>
<span
class=
"ng-cloak"
dropdown
keyboard-nav
>
<span
role=
"button"
data-toggle=
"dropdown"
dropdown-toggle
ng-if=
"!showAsIcon"
>
Sorted by {{sortBy | lowercase}}
<i
class=
"h-icon-arrow-drop-down"
></i>
</span>
<button
<button
type=
"button"
type=
"button"
class=
"top-bar__btn"
class=
"top-bar__btn"
ng-if=
"showAsIcon"
dropdown-toggle
dropdown-toggle
title=
"Sort by {{sort
B
y}}"
>
title=
"Sort by {{sort
Ke
y}}"
>
<i
class=
"h-icon-sort"
></i>
<i
class=
"h-icon-sort"
></i>
</button>
</button>
<div
class=
"dropdown-menu__top-arrow"
></div>
<div
class=
"dropdown-menu__top-arrow"
></div>
<ul
class=
"dropdown-menu"
<ul
class=
"dropdown-menu pull-right"
role=
"menu"
>
ng-class=
"showAsIcon ? 'pull-right' : 'pull-none'"
role=
"menu"
>
<li
class=
"dropdown-menu__row"
<li
class=
"dropdown-menu__row"
ng-repeat=
"
sortOption in sortOptions
"
ng-repeat=
"
key in sortKeysAvailable
"
ng-click=
"onChangeSort
By({sortBy: sortOption
})"
ng-click=
"onChangeSort
Key({sortKey: key
})"
><span
class=
"dropdown-menu-radio"
><span
class=
"dropdown-menu-radio"
ng-class=
"{'is-selected' : sort
Option === sortB
y}"
ng-class=
"{'is-selected' : sort
Key === ke
y}"
></span><a
class=
"dropdown-menu__link"
href=
""
>
{{
sortOption
}}
</a></li>
></span><a
class=
"dropdown-menu__link"
href=
""
>
{{
key
}}
</a></li>
</ul>
</ul>
</span>
</span>
h/templates/client/top_bar.html
View file @
0a894ccc
...
@@ -35,10 +35,9 @@
...
@@ -35,10 +35,9 @@
title=
"Filter the annotation list"
>
title=
"Filter the annotation list"
>
</simple-search>
</simple-search>
<sort-dropdown
<sort-dropdown
sort-options=
"sortOptions"
sort-keys-available=
"sortKeysAvailable"
sort-by=
"sortBy"
sort-key=
"sortKey"
show-as-icon=
"true"
on-change-sort-key=
"onChangeSortKey({sortKey: sortKey})"
>
on-change-sort-by=
"onChangeSortBy({sortBy: sortBy})"
>
</sort-dropdown>
</sort-dropdown>
<a
class=
"top-bar__btn"
<a
class=
"top-bar__btn"
ng-click=
"onSharePage()"
ng-click=
"onSharePage()"
...
...
h/templates/client/viewer.html
View file @
0a894ccc
...
@@ -16,13 +16,6 @@
...
@@ -16,13 +16,6 @@
total-count=
"topLevelThreadCount()"
total-count=
"topLevelThreadCount()"
>
>
</search-status-bar>
</search-status-bar>
<li
ng-if=
"isStream"
>
<sort-dropdown
sort-by=
"sort.name"
sort-options=
"sortOptions"
on-change-sort-by=
"sort.name = sortBy"
>
</sort-dropdown>
</li>
<li
class=
"annotation-unavailable-message"
<li
class=
"annotation-unavailable-message"
ng-if=
"selectedAnnotationUnavailable()"
>
ng-if=
"selectedAnnotationUnavailable()"
>
<div
class=
"annotation-unavailable-message__icon"
></div>
<div
class=
"annotation-unavailable-message__icon"
></div>
...
...
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