Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
M
mycard
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Locked Files
Issues
0
Issues
0
List
Boards
Labels
Service Desk
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Security & Compliance
Security & Compliance
Dependency List
License Compliance
Packages
Packages
List
Container Registry
Analytics
Analytics
CI / CD
Code Review
Insights
Issues
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
syntax_j
mycard
Commits
27727a69
Commit
27727a69
authored
Aug 27, 2021
by
神楽坂玲奈
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
candy libs
parent
f6b0a765
Changes
5
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
1152 additions
and
22 deletions
+1152
-22
app/candy.component.ts
app/candy.component.ts
+42
-8
date.format.js
date.format.js
+126
-0
package-lock.json
package-lock.json
+968
-9
package.json
package.json
+8
-2
systemjs.config.js
systemjs.config.js
+8
-3
No files found.
app/candy.component.ts
View file @
27727a69
...
@@ -23,11 +23,24 @@ $.fn.init = new Proxy($.fn.init, {
...
@@ -23,11 +23,24 @@ $.fn.init = new Proxy($.fn.init, {
window
[
'
jQuery
'
]
=
$
;
window
[
'
jQuery
'
]
=
$
;
const
Mustache
=
require
(
'
mustache
'
);
window
[
'
Mustache
'
]
=
Mustache
;
import
{
Component
,
ElementRef
,
Input
,
OnChanges
,
OnInit
,
SimpleChanges
,
ViewEncapsulation
}
from
'
@angular/core
'
;
import
{
Component
,
ElementRef
,
Input
,
OnChanges
,
OnInit
,
SimpleChanges
,
ViewEncapsulation
}
from
'
@angular/core
'
;
import
{
LoginService
}
from
'
./login.service
'
;
import
{
LoginService
}
from
'
./login.service
'
;
import
{
SettingsService
}
from
'
./settings.sevices
'
;
import
{
SettingsService
}
from
'
./settings.sevices
'
;
import
{
App
}
from
'
./app
'
;
import
{
App
}
from
'
./app
'
;
import
'
node_modules/candy/libs.min.js
'
;
// candy libs
import
'
strophe.js
'
;
import
'
strophejs-plugin-disco
'
;
import
'
strophejs-plugin-roster
'
;
import
'
strophejs-plugin-muc
'
;
import
'
strophejs-plugin-caps
'
;
import
'
jquery-i18n
'
;
import
'
date.format.js
'
;
// candy
import
'
node_modules/candy/candy.min.js
'
;
import
'
node_modules/candy/candy.min.js
'
;
import
'
node_modules/candy-shop/notifyme/candy.js
'
;
import
'
node_modules/candy-shop/notifyme/candy.js
'
;
import
'
node_modules/candy-shop/namecomplete/candy.js
'
;
import
'
node_modules/candy-shop/namecomplete/candy.js
'
;
...
@@ -35,15 +48,34 @@ import 'node_modules/candy-shop/modify-role/candy.js';
...
@@ -35,15 +48,34 @@ import 'node_modules/candy-shop/modify-role/candy.js';
import
'
node_modules/candy-shop/me-does/candy.js
'
;
import
'
node_modules/candy-shop/me-does/candy.js
'
;
import
'
node_modules/candy-shop/notifications/candy.js
'
;
import
'
node_modules/candy-shop/notifications/candy.js
'
;
import
'
node_modules/candy-shop/refocus/candy.js
'
;
import
'
node_modules/candy-shop/refocus/candy.js
'
;
// @ts-ignore
// window.Base64 = {
// encode: (data: string) => Buffer.from(data).toString('base64'),
// decode: (data: string) => Buffer.from(data, 'base64').toString()
// };
import
*
as
crypto
from
'
crypto
'
;
delete
window
[
'
jQuery
'
];
delete
window
[
'
jQuery
'
];
// Candy fix
// Candy fix
declare
const
Candy
:
any
,
CandyShop
:
any
,
Base64
:
any
;
declare
const
Candy
:
any
,
CandyShop
:
any
;
window
[
'
MD5
'
]
=
{
hexdigest
(
s
:
string
)
{
return
crypto
.
createHash
(
'
sha256
'
).
update
(
s
).
digest
(
'
hex
'
);
}
};
Base64
.
encode
=
(
data
:
string
)
=>
Buffer
.
from
(
data
).
toString
(
'
base64
'
);
Mustache
.
to_html
=
function
(
template
,
view
,
partials
,
send
)
{
Base64
.
decode
=
(
data
:
string
)
=>
Buffer
.
from
(
data
,
'
base64
'
).
toString
();
const
result
=
Mustache
.
render
(
template
,
view
,
partials
);
if
(
typeof
send
===
'
function
'
)
{
send
(
result
);
}
else
{
return
result
;
}
};
Candy
.
Util
.
getPosLeftAccordingToWindowBounds
=
new
Proxy
(
Candy
.
Util
.
getPosLeftAccordingToWindowBounds
,
{
Candy
.
Util
.
getPosLeftAccordingToWindowBounds
=
new
Proxy
(
Candy
.
Util
.
getPosLeftAccordingToWindowBounds
,
{
apply
(
target
,
thisArg
,
argumentsList
)
{
apply
(
target
,
thisArg
,
argumentsList
)
{
...
@@ -63,9 +95,9 @@ Candy.View.Pane.Roster.joinAnimation = function () {
...
@@ -63,9 +95,9 @@ Candy.View.Pane.Roster.joinAnimation = function () {
};
};
// 性能优化:禁用用户排序
// 性能优化:禁用用户排序
declare
const
Mustache
:
any
;
Candy
.
View
.
Pane
.
Roster
.
_insertUser
=
function
(
roomJid
:
string
,
roomId
:
string
,
user
:
any
,
userId
:
string
,
currentUser
:
any
)
{
Candy
.
View
.
Pane
.
Roster
.
_insertUser
=
function
(
roomJid
:
string
,
roomId
:
string
,
user
:
any
,
userId
:
string
,
currentUser
:
any
)
{
let
contact
=
user
.
getContact
();
let
contact
=
user
.
getContact
();
// @ts-ignore
let
html
=
Mustache
.
to_html
(
Candy
.
View
.
Template
.
Roster
.
user
,
{
let
html
=
Mustache
.
to_html
(
Candy
.
View
.
Template
.
Roster
.
user
,
{
roomId
:
roomId
,
roomId
:
roomId
,
userId
:
userId
,
userId
:
userId
,
...
@@ -98,8 +130,8 @@ Candy.View.Pane.Chat.increaseUnreadMessages = function (roomJid: string) {
...
@@ -98,8 +130,8 @@ Candy.View.Pane.Chat.increaseUnreadMessages = function (roomJid: string) {
// 性能优化:将收到消息时的滚动放进requestIdleCallback
// 性能优化:将收到消息时的滚动放进requestIdleCallback
declare
const
requestIdleCallback
:
Function
;
declare
const
requestIdleCallback
:
Function
;
Candy
.
View
.
Pane
.
Message
.
// tslint:disable-next-line:max-line-length
show
=
function
(
roomJid
:
any
,
name
:
any
,
message
:
any
,
xhtmlMessage
:
any
,
timestamp
:
any
,
from
:
any
,
carbon
:
any
,
stanza
:
any
)
{
Candy
.
View
.
Pane
.
Message
.
show
=
function
(
roomJid
:
any
,
name
:
any
,
message
:
any
,
xhtmlMessage
:
any
,
timestamp
:
any
,
from
:
any
,
carbon
:
any
,
stanza
:
any
)
{
message
=
Candy
.
Util
.
Parser
.
all
(
message
.
substring
(
0
,
Candy
.
View
.
getOptions
().
crop
.
message
.
body
));
message
=
Candy
.
Util
.
Parser
.
all
(
message
.
substring
(
0
,
Candy
.
View
.
getOptions
().
crop
.
message
.
body
));
if
(
Candy
.
View
.
getOptions
().
enableXHTML
===
true
&&
xhtmlMessage
)
{
if
(
Candy
.
View
.
getOptions
().
enableXHTML
===
true
&&
xhtmlMessage
)
{
xhtmlMessage
=
Candy
.
Util
.
parseAndCropXhtml
(
xhtmlMessage
,
Candy
.
View
.
getOptions
().
crop
.
message
.
body
);
xhtmlMessage
=
Candy
.
Util
.
parseAndCropXhtml
(
xhtmlMessage
,
Candy
.
View
.
getOptions
().
crop
.
message
.
body
);
...
@@ -152,6 +184,7 @@ Candy.View.Pane.Message.
...
@@ -152,6 +184,7 @@ Candy.View.Pane.Message.
stanza
:
stanza
stanza
:
stanza
};
};
$
(
Candy
).
triggerHandler
(
'
candy:view.message.before-render
'
,
renderEvtData
);
$
(
Candy
).
triggerHandler
(
'
candy:view.message.before-render
'
,
renderEvtData
);
// @ts-ignore
let
html
=
Mustache
.
to_html
(
renderEvtData
.
template
,
renderEvtData
.
templateData
);
let
html
=
Mustache
.
to_html
(
renderEvtData
.
template
,
renderEvtData
.
templateData
);
Candy
.
View
.
Pane
.
Room
.
appendToMessagePane
(
roomJid
,
html
);
Candy
.
View
.
Pane
.
Room
.
appendToMessagePane
(
roomJid
,
html
);
let
elem
=
Candy
.
View
.
Pane
.
Room
.
getPane
(
roomJid
,
'
.message-pane
'
).
children
().
last
();
let
elem
=
Candy
.
View
.
Pane
.
Room
.
getPane
(
roomJid
,
'
.message-pane
'
).
children
().
last
();
...
@@ -340,6 +373,7 @@ export class CandyComponent implements OnInit, OnChanges {
...
@@ -340,6 +373,7 @@ export class CandyComponent implements OnInit, OnChanges {
$
(
'
#restore
'
).
hide
();
$
(
'
#restore
'
).
hide
();
$
(
'
#maximize
'
).
show
();
$
(
'
#maximize
'
).
show
();
}
}
restore
():
void
{
restore
():
void
{
$
(
'
#candy
'
).
attr
(
'
data-minormax
'
,
'
default
'
);
$
(
'
#candy
'
).
attr
(
'
data-minormax
'
,
'
default
'
);
document
.
getElementById
(
'
candy-wrapper
'
)
!
.
style
!
.
height
=
this
.
height_default_window
;
document
.
getElementById
(
'
candy-wrapper
'
)
!
.
style
!
.
height
=
this
.
height_default_window
;
...
@@ -357,7 +391,7 @@ export class CandyComponent implements OnInit, OnChanges {
...
@@ -357,7 +391,7 @@ export class CandyComponent implements OnInit, OnChanges {
maximize
():
void
{
maximize
():
void
{
$
(
'
#candy
'
).
attr
(
'
data-minormax
'
,
'
max
'
);
$
(
'
#candy
'
).
attr
(
'
data-minormax
'
,
'
max
'
);
document
.
getElementById
(
'
candy-wrapper
'
)
!
.
style
!
.
height
=
'
calc( 100% - 180px )
'
;
document
.
getElementById
(
'
candy-wrapper
'
)
!
.
style
!
.
height
=
'
calc( 100% - 180px )
'
;
$
(
'
#mobile-roster-icon
'
).
css
(
'
display
'
,
'
block
'
);
$
(
'
#mobile-roster-icon
'
).
css
(
'
display
'
,
'
block
'
);
$
(
'
#chat-toolbar
'
).
css
(
'
display
'
,
'
block
'
);
$
(
'
#chat-toolbar
'
).
css
(
'
display
'
,
'
block
'
);
$
(
'
#chat-rooms
'
).
css
(
'
display
'
,
'
block
'
);
$
(
'
#chat-rooms
'
).
css
(
'
display
'
,
'
block
'
);
...
...
date.format.js
0 → 100644
View file @
27727a69
/*
* Date Format 1.2.3
* (c) 2007-2009 Steven Levithan <stevenlevithan.com>
* MIT license
*
* Includes enhancements by Scott Trenda <scott.trenda.net>
* and Kris Kowal <cixar.com/~kris.kowal/>
*
* Accepts a date, a mask, or a date and a mask.
* Returns a formatted version of the given date.
* The date defaults to the current date/time.
* The mask defaults to dateFormat.masks.default.
*/
var
dateFormat
=
function
()
{
var
token
=
/d
{1,4}
|m
{1,4}
|yy
(?:
yy
)?
|
([
HhMsTt
])\1?
|
[
LloSZ
]
|"
[^
"
]
*"|'
[^
'
]
*'/g
,
timezone
=
/
\b(?:[
PMCEA
][
SDP
]
T|
(?:
Pacific|Mountain|Central|Eastern|Atlantic
)
(?:
Standard|Daylight|Prevailing
)
Time|
(?:
GMT|UTC
)(?:[
-+
]\d{4})?)\b
/g
,
timezoneClip
=
/
[^
-+
\d
A-Z
]
/g
,
pad
=
function
(
val
,
len
)
{
val
=
String
(
val
);
len
=
len
||
2
;
while
(
val
.
length
<
len
)
val
=
"
0
"
+
val
;
return
val
;
};
// Regexes and supporting functions are cached through closure
return
function
(
date
,
mask
,
utc
)
{
var
dF
=
dateFormat
;
// You can't provide utc if you skip other args (use the "UTC:" mask prefix)
if
(
arguments
.
length
==
1
&&
Object
.
prototype
.
toString
.
call
(
date
)
==
"
[object String]
"
&&
!
/
\d
/
.
test
(
date
))
{
mask
=
date
;
date
=
undefined
;
}
// Passing date through Date applies Date.parse, if necessary
date
=
date
?
new
Date
(
date
)
:
new
Date
;
if
(
isNaN
(
date
))
throw
SyntaxError
(
"
invalid date
"
);
mask
=
String
(
dF
.
masks
[
mask
]
||
mask
||
dF
.
masks
[
"
default
"
]);
// Allow setting the utc argument via the mask
if
(
mask
.
slice
(
0
,
4
)
==
"
UTC:
"
)
{
mask
=
mask
.
slice
(
4
);
utc
=
true
;
}
var
_
=
utc
?
"
getUTC
"
:
"
get
"
,
d
=
date
[
_
+
"
Date
"
](),
D
=
date
[
_
+
"
Day
"
](),
m
=
date
[
_
+
"
Month
"
](),
y
=
date
[
_
+
"
FullYear
"
](),
H
=
date
[
_
+
"
Hours
"
](),
M
=
date
[
_
+
"
Minutes
"
](),
s
=
date
[
_
+
"
Seconds
"
](),
L
=
date
[
_
+
"
Milliseconds
"
](),
o
=
utc
?
0
:
date
.
getTimezoneOffset
(),
flags
=
{
d
:
d
,
dd
:
pad
(
d
),
ddd
:
dF
.
i18n
.
dayNames
[
D
],
dddd
:
dF
.
i18n
.
dayNames
[
D
+
7
],
m
:
m
+
1
,
mm
:
pad
(
m
+
1
),
mmm
:
dF
.
i18n
.
monthNames
[
m
],
mmmm
:
dF
.
i18n
.
monthNames
[
m
+
12
],
yy
:
String
(
y
).
slice
(
2
),
yyyy
:
y
,
h
:
H
%
12
||
12
,
hh
:
pad
(
H
%
12
||
12
),
H
:
H
,
HH
:
pad
(
H
),
M
:
M
,
MM
:
pad
(
M
),
s
:
s
,
ss
:
pad
(
s
),
l
:
pad
(
L
,
3
),
L
:
pad
(
L
>
99
?
Math
.
round
(
L
/
10
)
:
L
),
t
:
H
<
12
?
"
a
"
:
"
p
"
,
tt
:
H
<
12
?
"
am
"
:
"
pm
"
,
T
:
H
<
12
?
"
A
"
:
"
P
"
,
TT
:
H
<
12
?
"
AM
"
:
"
PM
"
,
Z
:
utc
?
"
UTC
"
:
(
String
(
date
).
match
(
timezone
)
||
[
""
]).
pop
().
replace
(
timezoneClip
,
""
),
o
:
(
o
>
0
?
"
-
"
:
"
+
"
)
+
pad
(
Math
.
floor
(
Math
.
abs
(
o
)
/
60
)
*
100
+
Math
.
abs
(
o
)
%
60
,
4
),
S
:
[
"
th
"
,
"
st
"
,
"
nd
"
,
"
rd
"
][
d
%
10
>
3
?
0
:
(
d
%
100
-
d
%
10
!=
10
)
*
d
%
10
]
};
return
mask
.
replace
(
token
,
function
(
$0
)
{
return
$0
in
flags
?
flags
[
$0
]
:
$0
.
slice
(
1
,
$0
.
length
-
1
);
});
};
}();
// Some common format strings
dateFormat
.
masks
=
{
"
default
"
:
"
ddd mmm dd yyyy HH:MM:ss
"
,
shortDate
:
"
m/d/yy
"
,
mediumDate
:
"
mmm d, yyyy
"
,
longDate
:
"
mmmm d, yyyy
"
,
fullDate
:
"
dddd, mmmm d, yyyy
"
,
shortTime
:
"
h:MM TT
"
,
mediumTime
:
"
h:MM:ss TT
"
,
longTime
:
"
h:MM:ss TT Z
"
,
isoDate
:
"
yyyy-mm-dd
"
,
isoTime
:
"
HH:MM:ss
"
,
isoDateTime
:
"
yyyy-mm-dd'T'HH:MM:ss
"
,
isoUtcDateTime
:
"
UTC:yyyy-mm-dd'T'HH:MM:ss'Z'
"
};
// Internationalization strings
dateFormat
.
i18n
=
{
dayNames
:
[
"
Sun
"
,
"
Mon
"
,
"
Tue
"
,
"
Wed
"
,
"
Thu
"
,
"
Fri
"
,
"
Sat
"
,
"
Sunday
"
,
"
Monday
"
,
"
Tuesday
"
,
"
Wednesday
"
,
"
Thursday
"
,
"
Friday
"
,
"
Saturday
"
],
monthNames
:
[
"
Jan
"
,
"
Feb
"
,
"
Mar
"
,
"
Apr
"
,
"
May
"
,
"
Jun
"
,
"
Jul
"
,
"
Aug
"
,
"
Sep
"
,
"
Oct
"
,
"
Nov
"
,
"
Dec
"
,
"
January
"
,
"
February
"
,
"
March
"
,
"
April
"
,
"
May
"
,
"
June
"
,
"
July
"
,
"
August
"
,
"
September
"
,
"
October
"
,
"
November
"
,
"
December
"
]
};
// For convenience...
Date
.
prototype
.
format
=
function
(
mask
,
utc
)
{
return
dateFormat
(
this
,
mask
,
utc
);
};
package-lock.json
View file @
27727a69
This diff is collapsed.
Click to expand it.
package.json
View file @
27727a69
...
@@ -48,6 +48,8 @@
...
@@ -48,6 +48,8 @@
"
glob
"
:
"
7.1.2
"
,
"
glob
"
:
"
7.1.2
"
,
"
ini
"
:
"
1.3.4
"
,
"
ini
"
:
"
1.3.4
"
,
"
jquery
"
:
"
2.2.4
"
,
"
jquery
"
:
"
2.2.4
"
,
"
jquery-i18n
"
:
"
recurser/jquery-i18n
"
,
"
lodash
"
:
"
^4.17.21
"
,
"
marked
"
:
"
0.3.6
"
,
"
marked
"
:
"
0.3.6
"
,
"
mustache
"
:
"
^4.2.0
"
,
"
mustache
"
:
"
^4.2.0
"
,
"
raven-js
"
:
"
3.16.1
"
,
"
raven-js
"
:
"
3.16.1
"
,
...
@@ -55,14 +57,18 @@
...
@@ -55,14 +57,18 @@
"
reconnecting-websocket
"
:
"
3.0.7
"
,
"
reconnecting-websocket
"
:
"
3.0.7
"
,
"
reflect-metadata
"
:
"
0.1.10
"
,
"
reflect-metadata
"
:
"
0.1.10
"
,
"
rxjs
"
:
"
5.4.2
"
,
"
rxjs
"
:
"
5.4.2
"
,
"
strophe.js
"
:
"
^1.4.2
"
,
"
strophejs-plugin-caps
"
:
"
^1.1.3
"
,
"
strophejs-plugin-disco
"
:
"
^0.0.2
"
,
"
strophejs-plugin-muc
"
:
"
^1.1.0
"
,
"
strophejs-plugin-roster
"
:
"
^1.1.0
"
,
"
systemjs
"
:
"
0.20.15
"
,
"
systemjs
"
:
"
0.20.15
"
,
"
systemjs-plugin-text
"
:
"
0.0.11
"
,
"
systemjs-plugin-text
"
:
"
0.0.11
"
,
"
tether
"
:
"
latest
"
,
"
tether
"
:
"
latest
"
,
"
typeahead.js
"
:
"
0.11.1
"
,
"
typeahead.js
"
:
"
0.11.1
"
,
"
uuid
"
:
"
3.1.0
"
,
"
uuid
"
:
"
3.1.0
"
,
"
vue
"
:
"
2.3.4
"
,
"
vue
"
:
"
2.3.4
"
,
"
zone.js
"
:
"
0.8.12
"
,
"
zone.js
"
:
"
0.8.12
"
"
lodash
"
:
"
^4.17.21
"
},
},
"devDependencies"
:
{
"devDependencies"
:
{
"
@angular/compiler-cli
"
:
"
4.2.6
"
,
"
@angular/compiler-cli
"
:
"
4.2.6
"
,
...
...
systemjs.config.js
View file @
27727a69
...
@@ -70,7 +70,6 @@ System.config({
...
@@ -70,7 +70,6 @@ System.config({
// other node.js libraries
// other node.js libraries
"
electron
"
:
"
@node/electron
"
,
"
electron
"
:
"
@node/electron
"
,
"
ini
"
:
"
@node/ini
"
,
"
ini
"
:
"
@node/ini
"
,
"
mustache
"
:
"
@node/mustache
"
,
"
lodash
"
:
"
@node/lodash
"
,
"
lodash
"
:
"
@node/lodash
"
,
"
mkdirp
"
:
"
@node/mkdirp
"
,
"
mkdirp
"
:
"
@node/mkdirp
"
,
"
aria2
"
:
"
@node/aria2
"
,
"
aria2
"
:
"
@node/aria2
"
,
...
@@ -83,9 +82,15 @@ System.config({
...
@@ -83,9 +82,15 @@ System.config({
'
reconnecting-websocket
'
:
'
npm:reconnecting-websocket/dist/index.js
'
,
'
reconnecting-websocket
'
:
'
npm:reconnecting-websocket/dist/index.js
'
,
'
popper.js
'
:
'
npm:popper.js/dist/umd/popper.min.js
'
,
'
popper.js
'
:
'
npm:popper.js/dist/umd/popper.min.js
'
,
// 'typeahead.js': '@node/typeahead.js'
// 'typeahead.js': '@node/typeahead.js'
'
raven-js
'
:
'
npm:raven-js
'
,
'
raven-js
'
:
'
npm:raven-js
'
'
strophe.js
'
:
'
npm:strophe.js/dist/strophe.umd.js
'
,
'
strophejs-plugin-disco
'
:
'
npm:strophejs-plugin-disco/lib/strophe.disco.js
'
,
'
strophejs-plugin-roster
'
:
'
npm:strophejs-plugin-roster/lib/strophe.roster.js
'
,
'
strophejs-plugin-muc
'
:
'
npm:strophejs-plugin-muc/lib/strophe.muc.js
'
,
'
strophejs-plugin-caps
'
:
'
npm:strophejs-plugin-caps/lib/strophe.caps.js
'
,
'
jquery-i18n
'
:
'
npm:jquery-i18n/jquery.i18n.js
'
,
"
mustache
"
:
"
npm:mustache/mustache.js
"
},
},
// packages tells the System loader how to load when no filename and/or no extension
// packages tells the System loader how to load when no filename and/or no extension
packages
:
{
packages
:
{
...
...
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