Commit 7019ca64 authored by Alexandre Delanoë's avatar Alexandre Delanoë

[MERGE] ngrams table with grouping logic

parents bbce1b23 e441fc2d
{
"presets": ["es2015"]
}
......@@ -27,3 +27,8 @@
#user-page-info {
margin-top : 38px;
}
.tableHeader {
background-color : blue;
color: white;
}
......@@ -37,7 +37,7 @@
content: " ";
position: absolute;
width: 1px;
background-color: #000;
background-color: "#000";
top: 5px;
bottom: 7px;
height: 7px;
......@@ -49,15 +49,13 @@
left: -10px;
width: 10px;
height: 1px;
background-color: #000;
background-color: "#000";
top: 12px;
}
</style>
</head>
<body>
<div id="app" class ="container"></div>
<div id="app" class ="container-fluid"></div>
<script src="bundle.js"></script>
<script src="js/bootstrap-native.min.js"></script>
</body>
......
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"dependencies": {
"create-react-class": "^15.6.2",
"echarts": "^3.8.5",
"echarts-for-react": "^2.0.0",
"prop-types": "15.6.0",
"react": "^16.2.0",
"react-addons-css-transition-group": "^16.2.0",
"react-dom": "^16.2.0",
"react-echarts-v3": "^1.0.14"
}
"dependencies": {
"create-react-class": "^15.6.2",
"echarts": "^3.8.5",
"echarts-for-react": "^2.0.0",
"imports-loader": "^0.7.1",
"prop-types": "15.6.0",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-echarts-v3": "^1.0.14",
"sigma": "^1.2.1",
"graph-explorer": "git+ssh://git@gitlab.iscpif.fr:20022/gargantext/graphExplorer.git"
},
"browserify": {
"transform": [
[
"babelify",
{
"presets": [
"es2015",
"stage-0",
"react"
]
}
]
]
},
"babel": {
"presets": [
"es2015",
"stage-0",
"react"
]
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"babel-runtime": "^6.26.0",
"babelify": "^8.0.0"
}
}
module AnnotationDocumentView where
module DocAnnotation where
import Prelude hiding (div)
import React (ReactElement)
......
......@@ -143,7 +143,8 @@ performAction (ChangePageSize ps) _ _ = void (cotransform (\state -> changePage
performAction (ChangePage p) _ _ = void (cotransform (\(TableData td) -> TableData $ td { currentPage = p} ))
performAction LoadData _ _ = void do
res <- lift $ loadData
res <- lift $ loadData "http://localhost:8008/corpus/452132/facet/documents/table"
--res <- lift $ loadData "http://localhost:8009/corpus/1/facet/documents/table"
case res of
Left err -> cotransform $ \(state) -> state
Right resData -> do
......@@ -379,13 +380,11 @@ showRow {row : (Corpus c), delete} =
true -> "fas "
false -> "far "
loadData :: forall eff. Aff ( console :: CONSOLE, ajax :: AJAX| eff) (Either String (Array Response))
loadData = do
loadData :: forall eff. String -> Aff ( console :: CONSOLE, ajax :: AJAX| eff) (Either String (Array Response))
loadData url = do
affResp <- liftAff $ attempt $ affjax defaultRequest
{ method = Left GET
, url = "http://localhost:8009/corpus/1/facet/documents/table"
, url = url
, headers = [ ContentType applicationJSON
, Accept applicationJSON
-- , RequestHeader "Authorization" $ "Bearer " <> token
......
This source diff could not be displayed because it is too large. You can view the blob instead.
module GraphExplorer where
import React.DOM (button, button', div, form', input, li', menu, text, ul, ul')
import React.DOM.Props (_id, _type, className, name, placeholder, value)
import Thermite (Spec, defaultPerformAction, simpleSpec)
import Prelude hiding (div)
newtype State = State {mode :: String}
import Control.Monad.Aff (runAff)
import Control.Monad.Aff.Class (liftAff)
import Control.Monad.Aff.Console (CONSOLE)
import Control.Monad.Cont.Trans (lift)
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Class (liftEff)
import Control.Monad.Eff.Console (log)
import DOM (DOM)
import DOM.File.FileReader (fileReader, readAsText, result)
import DOM.File.Types (File, fileToBlob)
import React (ReactClass, createElement)
import React.DOM (button, button', div, form', input, li, li', menu, text, ul')
import React.DOM.Props (_id, _type, className, name, onChange, onClick, placeholder, style, value)
import Thermite (PerformAction, Render, Spec, modifyState, simpleSpec)
data Action = NoOp
foreign import data GraphData :: Type
foreign import initialGraph :: GraphData
foreign import initialFile :: File
foreign import getFile :: forall e. e -> File
foreign import parseJSON :: forall eff a. a -> Eff eff GraphData
foreign import graphExplorerComponent :: ReactClass {graph :: GraphData, mode :: String}
foreign import logger :: forall a eff. a -> Eff eff Unit
newtype State = State {mode :: String, graph :: GraphData, file :: File}
data Action
= SetGraph
| SetFile File
initialState :: State
initialState = State {mode : "select"}
initialState = State {mode : "select", graph : initialGraph, file : initialFile}
reader :: forall eff. File -> Eff (console :: CONSOLE, dom :: DOM | eff) GraphData
reader f = do
fr <- fileReader
readAsText (fileToBlob f) fr
res <- result fr
--log $ show res
let da = parseJSON res
logger da
da
spec :: forall eff props. Spec eff State props Action
spec = simpleSpec defaultPerformAction render
spec :: forall eff props. Spec (console :: CONSOLE, dom :: DOM | eff) State props Action
spec = simpleSpec performAction render
where
render _ _ _ _ =
render :: Render State props Action
render d _ (State st) _ =
[ div [className "row"] [
div [className "col-md-12"]
[ menu [_id "toolbar"]
[ ul'
[ li'
[ li [style {display : "inline-block"}]
[ form'
[ input [_type "file", name "file", value ""] []
, input [_type "submit", value "submit"] []
[ input [_type "file", name "file", onChange (\e -> d $ SetFile $ getFile e)] []
, input [_type "button", value "submit", onClick \_ -> d SetGraph] []
]
]
, li'
......@@ -48,12 +90,24 @@ spec = simpleSpec defaultPerformAction render
]
, div [className "row"]
[ div [className "col-md-8"]
[ div [] [text "GraphExplorer here...."]
[ div [style {border : "1px black solid", height: "90%"}]
[ text "GraphExplorer here...."
, createElement graphExplorerComponent { graph : st.graph
, mode : st.mode
} []
]
]
, div [className "col-md-4"]
[ div [_id "sidepanel"]
[ div [_id "sidepanel", style {border : "1px black solid", height: "90%"}]
[ text "SidePanel for contextual information"
]
]
]
]
performAction :: PerformAction (console :: CONSOLE, dom :: DOM | eff) State props Action
performAction SetGraph _ (State st) = void do
gd <- liftEff $ reader st.file
modifyState \(State s) -> State $ s {graph = gd}
performAction (SetFile f) _ _ = void do
modifyState \(State s) -> State $ s {file = f}
......@@ -4,7 +4,7 @@ import DOM
import Gargantext.Data.Lang
import AddCorpusview as AC
import AnnotationDocumentView as D
import DocAnnotation as D
import Control.Monad.Cont.Trans (lift)
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Class (liftEff)
......@@ -35,6 +35,7 @@ import Thermite (PerformAction, Render, Spec, _render, cotransform, defaultPerfo
import Unsafe.Coerce (unsafeCoerce)
import UserPage as UP
import GraphExplorer as GE
import NgramsTable as NG
type E e = (dom :: DOM, ajax :: AJAX, console :: CONSOLE | e)
......@@ -46,14 +47,15 @@ type AppState =
, docViewState :: DV.State
, searchState :: S.State
, userPage :: UP.State
, annotationdocumentView :: D.State
, ntreeView :: NT.State
, tabview :: TV.State
, search :: String
, docAnnotationView :: D.State
, ntreeView :: NT.State
, tabview :: TV.State
, search :: String
, corpusAnalysis :: CA.State
, showLogin :: Boolean
, showCorpus :: Boolean
, graphExplorer :: GE.State
, showLogin :: Boolean
, showCorpus :: Boolean
, graphExplorer :: GE.State
, ngState :: NG.State
}
initAppState :: AppState
......@@ -65,14 +67,15 @@ initAppState =
, docViewState : DV.tdata
, searchState : S.initialState
, userPage : UP.initialState
, annotationdocumentView : D.initialState
, ntreeView : NT.exampleTree
, tabview : TV.initialState
, search : ""
, docAnnotationView : D.initialState
, ntreeView : NT.exampleTree
, tabview : TV.initialState
, search : ""
, corpusAnalysis : CA.initialState
, showLogin : false
, showCorpus : false
, graphExplorer : GE.initialState
, showLogin : false
, showCorpus : false
, graphExplorer : GE.initialState
, ngState : NG.initialState
}
data Action
......@@ -84,7 +87,7 @@ data Action
| DocViewA DV.Action
| SearchA S.Action
| UserPageA UP.Action
| AnnotationDocumentViewA D.Action
| DocAnnotationViewA D.Action
| TreeViewA NT.Action
| TabViewA TV.Action
| GraphExplorerA GE.Action
......@@ -93,6 +96,7 @@ data Action
| CorpusAnalysisA CA.Action
| ShowLogin
| ShowAddcorpus
| NgramsA NG.Action
......@@ -191,14 +195,14 @@ _userPageAction = prism UserPageA \action ->
_-> Left action
_annotationdocumentviewState :: Lens' AppState D.State
_annotationdocumentviewState = lens (\s -> s.annotationdocumentView) (\s ss -> s{annotationdocumentView = ss})
_docAnnotationViewState :: Lens' AppState D.State
_docAnnotationViewState = lens (\s -> s.docAnnotationView) (\s ss -> s{docAnnotationView = ss})
_annotationdocumentviewAction :: Prism' Action D.Action
_annotationdocumentviewAction = prism AnnotationDocumentViewA \action ->
_docAnnotationViewAction :: Prism' Action D.Action
_docAnnotationViewAction = prism DocAnnotationViewA \action ->
case action of
AnnotationDocumentViewA caction -> Right caction
DocAnnotationViewA caction -> Right caction
_-> Left action
......@@ -246,6 +250,17 @@ _graphExplorerAction = prism GraphExplorerA \action ->
_-> Left action
_ngState :: Lens' AppState NG.State
_ngState = lens (\s -> s.ngState) (\s ss -> s{ngState = ss})
_ngAction :: Prism' Action NG.Action
_ngAction = prism NgramsA \action ->
case action of
NgramsA caction -> Right caction
_-> Left action
pagesComponent :: forall props eff. AppState -> Spec (E eff) AppState props Action
pagesComponent s =
case s.currentRoute of
......@@ -265,11 +280,13 @@ pagesComponent s =
-- selectSpec AddCorpus = layout0 $ focus _addCorpusState _addCorpusAction AC.layoutAddcorpus
selectSpec DocView = layout0 $ focus _docViewState _docViewAction DV.layoutDocview
selectSpec UserPage = layout0 $ focus _userPageState _userPageAction UP.layoutUser
selectSpec (AnnotationDocumentView i) = layout0 $ focus _annotationdocumentviewState _annotationdocumentviewAction D.docview
selectSpec (DocAnnotation i) = layout0 $ focus _docAnnotationViewState _docAnnotationViewAction D.docview
selectSpec Tabview = layout0 $ focus _tabviewState _tabviewAction TV.tab1
-- To be removed
selectSpec SearchView = layout0 $ focus _searchState _searchAction S.searchSpec
selectSpec NGramsTable = layout0 $ focus _ngState _ngAction NG.ngramsTableSpec
selectSpec PGraphExplorer = focus _graphExplorerState _graphExplorerAction GE.spec
selectSpec _ = simpleSpec defaultPerformAction defaultRender
routingSpec :: forall props eff. Spec (dom :: DOM |eff) AppState props Action
......@@ -294,9 +311,9 @@ layout0 layout =
else outerLayout1
, rs bs ]
ls = over _render \render d p s c ->
[div [className "col-md-3"] (render d p s c)]
[div [className "col-md-2"] (render d p s c)]
rs = over _render \render d p s c ->
[ div [className "col-md-8"] (render d p s c) ]
[ div [className "col-md-10"] (render d p s c) ]
cont = over _render \render d p s c ->
[ div [ className "row" ] (render d p s c) ]
......@@ -577,9 +594,9 @@ dispatchAction dispatcher _ UserPage = do
_ <- dispatcher $ UserPageA $ UP.NoOp
pure unit
dispatchAction dispatcher _ (AnnotationDocumentView i) = do
_ <- dispatcher $ SetRoute $ AnnotationDocumentView i
_ <- dispatcher $ AnnotationDocumentViewA $ D.NoOp
dispatchAction dispatcher _ (DocAnnotation i) = do
_ <- dispatcher $ SetRoute $ DocAnnotation i
_ <- dispatcher $ DocAnnotationViewA $ D.NoOp
pure unit
......@@ -598,3 +615,8 @@ dispatchAction dispatcher _ PGraphExplorer = do
_ <- dispatcher $ SetRoute $ PGraphExplorer
--_ <- dispatcher $ GraphExplorerA $ GE.NoOp
pure unit
dispatchAction dispatcher _ NGramsTable = do
_ <- dispatcher $ SetRoute $ NGramsTable
_ <- dispatcher $ NgramsA $ NG.NoOp
pure unit
module NgramsItem where
import Prelude
import Control.Monad.Eff.Console (CONSOLE)
import DOM (DOM)
import Data.Newtype (class Newtype)
import Network.HTTP.Affjax (AJAX)
import React (ReactElement)
import React.DOM (input, span, td, text, tr)
import React.DOM.Props (_type, checked, className, color, onChange, style, title)
import Thermite (PerformAction, Render, Spec, modifyState, simpleSpec)
import Utils (getter, setter)
newtype State = State
{ term :: Term
}
initialState :: State
initialState = State {term : Term {id : 10, term : "hello", occurrence : 10, _type : None, children : []}}
newtype Term = Term {id :: Int, term :: String, occurrence :: Int, _type :: TermType, children :: Array Term}
derive instance newtypeTerm :: Newtype Term _
data TermType = MapTerm | StopTerm | None
derive instance eqTermType :: Eq TermType
instance showTermType :: Show TermType where
show MapTerm = "MapTerm"
show StopTerm = "StopTerm"
show None = "None"
data Action
= SetMap Boolean
| SetStop Boolean
performAction :: forall eff props. PerformAction ( console :: CONSOLE , ajax :: AJAX, dom :: DOM | eff ) State props Action
performAction (SetMap b) _ _ = void do
modifyState \(State s) -> State s {term = setter (_{_type = (if b then MapTerm else None)}) s.term}
performAction (SetStop b) _ _ = void do
modifyState \(State s) -> State s {term = setter (_{_type = (if b then StopTerm else None)}) s.term}
ngramsItemSpec :: forall props eff . Spec (console::CONSOLE, ajax::AJAX, dom::DOM | eff) State props Action
ngramsItemSpec = simpleSpec performAction render
where
render :: Render State props Action
render dispatch _ (State state) _ =
[
tr []
[ td [] [ checkbox_map]
, td [] [ checkbox_stop]
, td [] [ dispTerm (getter _.term state.term) (getter _._type state.term) ]
, td [] [ text $ show $ getter _.occurrence state.term]
]
]
where
checkbox_map =
input [ _type "checkbox"
, className "checkbox"
, checked $ getter _._type state.term == MapTerm
, title "Mark as completed"
, onChange $ dispatch <<< ( const $ SetMap $ not (getter _._type state.term == MapTerm))
] []
checkbox_stop =
input
[ _type "checkbox"
, className "checkbox"
, checked $ getter _._type state.term == StopTerm
, title "Mark as completed"
, onChange $ dispatch <<< ( const $ SetStop $ not (getter _._type state.term == StopTerm))
] []
dispTerm :: String -> TermType -> ReactElement
dispTerm term MapTerm = span [style {color :"green"}] [text $ term]
dispTerm term StopTerm = span [style {color :"red", textDecoration : "line-through"}] [text $ term]
dispTerm term None = span [style {color :"black"}] [text term]
This diff is collapsed.
......@@ -22,11 +22,11 @@ data Routes
| DocView
| SearchView
| UserPage
| AnnotationDocumentView Int
| DocAnnotation Int
| Tabview
| CorpusAnalysis
| PGraphExplorer
| NGramsTable
instance showRoutes :: Show Routes where
show Home = "Home"
......@@ -35,41 +35,31 @@ instance showRoutes :: Show Routes where
show DocView = "DocView"
show SearchView = "SearchView"
show UserPage = "UserPage"
show (AnnotationDocumentView i) = "DocumentView"
show Tabview = "Tabview"
show (DocAnnotation i)= "DocumentView"
show Tabview = "Tabview"
show CorpusAnalysis = "corpus"
show PGraphExplorer = "graphExplorer"
show PGraphExplorer = "graphExplorer"
show NGramsTable = "NGramsTable"
int :: Match Int
int = floor <$> num
routing :: Match Routes
routing =
loginRoute
<|> tabview
<|> documentView
<|> userPageRoute
<|> searchRoute
<|> docviewRoute
<|> addcorpusRoute
<|> corpusAnalysis
<|> graphExplorer
<|> home
Login <$ route "login"
<|> Tabview <$ route "tabview"
<|> DocAnnotation <$> (route "documentView" *> int)
<|> UserPage <$ route "userPage"
<|> SearchView <$ route "search"
<|> DocView <$ route "docView"
<|> AddCorpus <$ route "addCorpus"
<|> CorpusAnalysis <$ route "corpus"
<|> PGraphExplorer <$ route "graphExplorer"
<|> NGramsTable <$ route "ngrams"
<|> Home <$ lit ""
where
tabview = Tabview <$ route "tabview"
documentView = AnnotationDocumentView <$> (route "documentView" *> int)
userPageRoute = UserPage <$ route "userPage"
searchRoute = SearchView <$ route "search"
docviewRoute = DocView <$ route "docView"
addcorpusRoute = AddCorpus <$ route "addCorpus"
loginRoute = Login <$ route "login"
corpusAnalysis = CorpusAnalysis <$ route "corpus"
graphExplorer = PGraphExplorer <$ route "graphExplorer"
home = Home <$ lit ""
route str = lit "" *> lit str
routeHandler :: forall e. ( Maybe Routes -> Routes -> Eff
( dom :: DOM
, console :: CONSOLE
......
module Utils where
import Prelude
import Data.Newtype (class Newtype, unwrap, wrap)
setterv :: forall nt record field. Newtype nt record => (record -> field -> record) -> field -> nt -> nt
setterv fn v t = (setter (flip fn v) t)
setter :: forall nt record. Newtype nt record => (record -> record) -> nt -> nt
setter fn = wrap <<< fn <<< unwrap
getter :: forall record field nt. Newtype nt record => (record -> field) -> nt -> field
getter fn = fn <<< unwrap
var React = require('react');
var PropTypes = require('prop-types');
var graphExplorer = require('graph-explorer');
console.log(graphExplorer);
const GraphExplorer = graphExplorer.default;
class ReactGraphExplorer extends React.Component {
constructor(props) {
super(props);
this.ge = new GraphExplorer(props.settings, props.handlers);
this.state = { graph: props.graph, mode: props.mode };
this.initRenderer = this.initRenderer.bind(this);
}
componentWillReceiveProps(nextProps) {
console.log(this.ge);
if (nextProps.graph) {
this.setState({ graph: nextProps.graph }, () => {
this.ge.loadGraph(this.state.graph);
this.ge.clusterize();
this.ge.spatialize();
});
}
this.setState({ mode: nextProps.mode }, () => {
this.ge.sigma.settings('mode', this.state.mode);
});
}
initRenderer(container) {
this.ge.addRenderer(container);
}
render() {
return React.createElement('div', { id: 'ge-container', ref: this.initRenderer }, null);
}
}
ReactGraphExplorer.propTypes = {
graph: PropTypes.shape({
nodes: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
label: PropTypes.string,
x: PropTypes.number,
y: PropTypes.number,
size: PropTypes.number,
type: PropTypes.string,
attributes: PropTypes.object // TODO(lucas): Specify this further.
})
).isRequired,
edges: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
source: PropTypes.string.isRequired,
target: PropTypes.string.isRequired,
weight: PropTypes.number // NOTE(lucas): required?
}).isRequired
)
}),
settings: PropTypes.shape({
// TODO(lucas)
}),
mode: PropTypes.string,
handlers: PropTypes.shape({
overNode: PropTypes.func,
outNode: PropTypes.func,
clickNode: PropTypes.func,
doubleClickNode: PropTypes.func,
rightClickNode: PropTypes.func,
overEdge: PropTypes.func,
outEdge: PropTypes.func,
clickEdge: PropTypes.func,
doubleClickEdge: PropTypes.func,
rightClickEdge: PropTypes.func,
clickStage: PropTypes.func,
doubleClickStage: PropTypes.func,
rightClickStage: PropTypes.func
})
};
export default ReactGraphExplorer;
// In production, we register a service worker to serve assets from local cache.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the "N+1" visit to a page, since previously
// cached resources are updated in the background.
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl);
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl);
}
});
}
}
function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get('content-type').indexOf('javascript') === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment