Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
RocketChat
Rocket.Chat.ReactNative
Commits
04db33a6
Unverified
Commit
04db33a6
authored
Jan 24, 2022
by
Diego Mello
Committed by
GitHub
Jan 24, 2022
Browse files
Merge 4.24.0 into master (#3648)
parent
857394ab
Changes
205
Hide whitespace changes
Inline
Side-by-side
.eslintrc.js
View file @
04db33a6
...
...
@@ -17,14 +17,15 @@ module.exports = {
legacyDecorators
:
true
}
},
plugins
:
[
'
react
'
,
'
jsx-a11y
'
,
'
import
'
,
'
react-native
'
,
'
@babel
'
],
plugins
:
[
'
react
'
,
'
jsx-a11y
'
,
'
import
'
,
'
react-native
'
,
'
@babel
'
,
'
jest
'
],
env
:
{
browser
:
true
,
commonjs
:
true
,
es6
:
true
,
node
:
true
,
jquery
:
true
,
mocha
:
true
mocha
:
true
,
'
jest/globals
'
:
true
},
rules
:
{
'
import/extensions
'
:
[
...
...
android/app/build.gradle
View file @
04db33a6
...
...
@@ -144,7 +144,7 @@ android {
minSdkVersion
rootProject
.
ext
.
minSdkVersion
targetSdkVersion
rootProject
.
ext
.
targetSdkVersion
versionCode
VERSIONCODE
as
Integer
versionName
"4.2
3
.0"
versionName
"4.2
4
.0"
vectorDrawables
.
useSupportLibrary
=
true
if
(!
isFoss
)
{
manifestPlaceholders
=
[
BugsnagAPIKey:
BugsnagAPIKey
as
String
]
...
...
android/app/src/main/java/chat/rocket/reactnative/networking/SSLPinningModule.java
View file @
04db33a6
...
...
@@ -11,9 +11,12 @@ import com.facebook.react.bridge.ReactMethod;
import
com.facebook.react.bridge.Promise
;
import
java.net.Socket
;
import
java.security.KeyStore
;
import
java.security.Principal
;
import
java.security.cert.CertificateException
;
import
java.security.cert.X509Certificate
;
import
javax.net.ssl.TrustManagerFactory
;
import
javax.net.ssl.X509ExtendedKeyManager
;
import
java.security.PrivateKey
;
import
javax.net.ssl.SSLContext
;
...
...
@@ -21,11 +24,12 @@ import javax.net.ssl.X509TrustManager;
import
javax.net.ssl.SSLSocketFactory
;
import
javax.net.ssl.TrustManager
;
import
okhttp3.OkHttpClient
;
import
java.lang.InterruptedException
;
import
android.app.Activity
;
import
javax.net.ssl.KeyManager
;
import
android.security.KeyChain
;
import
android.security.KeyChainAliasCallback
;
import
java.util.Arrays
;
import
java.util.concurrent.TimeUnit
;
import
com.RNFetchBlob.RNFetchBlob
;
...
...
@@ -52,8 +56,9 @@ public class SSLPinningModule extends ReactContextBaseJavaModule implements KeyC
public
void
apply
(
OkHttpClient
.
Builder
builder
)
{
if
(
alias
!=
null
)
{
SSLSocketFactory
sslSocketFactory
=
getSSLFactory
(
alias
);
X509TrustManager
trustManager
=
getTrustManagerFactory
();
if
(
sslSocketFactory
!=
null
)
{
builder
.
sslSocketFactory
(
sslSocketFactory
);
builder
.
sslSocketFactory
(
sslSocketFactory
,
trustManager
);
}
}
}
...
...
@@ -68,8 +73,9 @@ public class SSLPinningModule extends ReactContextBaseJavaModule implements KeyC
if
(
alias
!=
null
)
{
SSLSocketFactory
sslSocketFactory
=
getSSLFactory
(
alias
);
X509TrustManager
trustManager
=
getTrustManagerFactory
();
if
(
sslSocketFactory
!=
null
)
{
builder
.
sslSocketFactory
(
sslSocketFactory
);
builder
.
sslSocketFactory
(
sslSocketFactory
,
trustManager
);
}
}
...
...
@@ -162,25 +168,9 @@ public class SSLPinningModule extends ReactContextBaseJavaModule implements KeyC
}
};
final
TrustManager
[]
trustAllCerts
=
new
TrustManager
[]
{
new
X509TrustManager
()
{
@Override
public
void
checkClientTrusted
(
java
.
security
.
cert
.
X509Certificate
[]
chain
,
String
authType
)
throws
CertificateException
{
}
@Override
public
void
checkServerTrusted
(
java
.
security
.
cert
.
X509Certificate
[]
chain
,
String
authType
)
throws
CertificateException
{
}
@Override
public
java
.
security
.
cert
.
X509Certificate
[]
getAcceptedIssuers
()
{
return
certChain
;
}
}
};
final
X509TrustManager
trustManager
=
getTrustManagerFactory
();
final
SSLContext
sslContext
=
SSLContext
.
getInstance
(
"TLS"
);
sslContext
.
init
(
new
KeyManager
[]{
keyManager
},
trustAllCerts
,
new
java
.
security
.
SecureRandom
());
sslContext
.
init
(
new
KeyManager
[]{
keyManager
},
new
TrustManager
[]{
trustManager
}
,
new
java
.
security
.
SecureRandom
());
SSLContext
.
setDefault
(
sslContext
);
final
SSLSocketFactory
sslSocketFactory
=
sslContext
.
getSocketFactory
();
...
...
@@ -190,4 +180,19 @@ public class SSLPinningModule extends ReactContextBaseJavaModule implements KeyC
return
null
;
}
}
public
static
X509TrustManager
getTrustManagerFactory
()
{
try
{
TrustManagerFactory
trustManagerFactory
=
TrustManagerFactory
.
getInstance
(
TrustManagerFactory
.
getDefaultAlgorithm
());
trustManagerFactory
.
init
((
KeyStore
)
null
);
TrustManager
[]
trustManagers
=
trustManagerFactory
.
getTrustManagers
();
if
(
trustManagers
.
length
!=
1
||
!(
trustManagers
[
0
]
instanceof
X509TrustManager
))
{
throw
new
IllegalStateException
(
"Unexpected default trust managers:"
+
Arrays
.
toString
(
trustManagers
));
}
final
X509TrustManager
trustManager
=
(
X509TrustManager
)
trustManagers
[
0
];
return
trustManager
;
}
catch
(
Exception
e
)
{
return
null
;
}
}
}
app/AppContainer.tsx
View file @
04db33a6
...
...
@@ -3,7 +3,7 @@ import { NavigationContainer } from '@react-navigation/native';
import
{
createStackNavigator
}
from
'
@react-navigation/stack
'
;
import
{
connect
}
from
'
react-redux
'
;
import
{
SetUsernameStackParamList
,
StackParamList
}
from
'
./navigationTypes
'
;
import
{
SetUsernameStackParamList
,
StackParamList
}
from
'
./
definitions/
navigationTypes
'
;
import
Navigation
from
'
./lib/Navigation
'
;
import
{
defaultHeader
,
getActiveRouteName
,
navigationTheme
}
from
'
./utils/navigation
'
;
import
{
ROOT_INSIDE
,
ROOT_LOADING
,
ROOT_OUTSIDE
,
ROOT_SET_USERNAME
}
from
'
./actions/app
'
;
...
...
app/actions/actionsTypes.
j
s
→
app/actions/actionsTypes.
t
s
View file @
04db33a6
...
...
@@ -2,8 +2,8 @@ const REQUEST = 'REQUEST';
const
SUCCESS
=
'
SUCCESS
'
;
const
FAILURE
=
'
FAILURE
'
;
const
defaultTypes
=
[
REQUEST
,
SUCCESS
,
FAILURE
];
function
createRequestTypes
(
base
,
types
=
defaultTypes
)
{
const
res
=
{};
function
createRequestTypes
(
base
=
{}
,
types
=
defaultTypes
)
:
Record
<
any
,
any
>
{
const
res
:
Record
<
any
,
any
>
=
{};
types
.
forEach
(
type
=>
(
res
[
type
]
=
`
${
base
}
_
${
type
}
`
));
return
res
;
}
...
...
app/actions/activeUsers.js
deleted
100644 → 0
View file @
857394ab
import
{
SET_ACTIVE_USERS
}
from
'
./actionsTypes
'
;
export
function
setActiveUsers
(
activeUsers
)
{
return
{
type
:
SET_ACTIVE_USERS
,
activeUsers
};
}
app/actions/activeUsers.ts
0 → 100644
View file @
04db33a6
import
{
Action
}
from
'
redux
'
;
import
{
IActiveUsers
}
from
'
../reducers/activeUsers
'
;
import
{
SET_ACTIVE_USERS
}
from
'
./actionsTypes
'
;
export
interface
ISetActiveUsers
extends
Action
{
activeUsers
:
IActiveUsers
;
}
export
type
TActionActiveUsers
=
ISetActiveUsers
;
export
const
setActiveUsers
=
(
activeUsers
:
IActiveUsers
):
ISetActiveUsers
=>
({
type
:
SET_ACTIVE_USERS
,
activeUsers
});
app/actions/selectedUsers.
j
s
→
app/actions/selectedUsers.
t
s
View file @
04db33a6
import
{
Action
}
from
'
redux
'
;
import
{
ISelectedUser
}
from
'
../reducers/selectedUsers
'
;
import
*
as
types
from
'
./actionsTypes
'
;
export
function
addUser
(
user
)
{
type
TUser
=
{
user
:
ISelectedUser
;
};
type
TAction
=
Action
&
TUser
;
interface
ISetLoading
extends
Action
{
loading
:
boolean
;
}
export
type
TActionSelectedUsers
=
TAction
&
ISetLoading
;
export
function
addUser
(
user
:
ISelectedUser
):
TAction
{
return
{
type
:
types
.
SELECTED_USERS
.
ADD_USER
,
user
};
}
export
function
removeUser
(
user
)
{
export
function
removeUser
(
user
:
ISelectedUser
):
TAction
{
return
{
type
:
types
.
SELECTED_USERS
.
REMOVE_USER
,
user
};
}
export
function
reset
()
{
export
function
reset
()
:
Action
{
return
{
type
:
types
.
SELECTED_USERS
.
RESET
};
}
export
function
setLoading
(
loading
)
{
export
function
setLoading
(
loading
:
boolean
):
ISetLoading
{
return
{
type
:
types
.
SELECTED_USERS
.
SET_LOADING
,
loading
...
...
app/constants/constantDisplayMode.js
deleted
100644 → 0
View file @
857394ab
export
const
DISPLAY_MODE_CONDENSED
=
'
condensed
'
;
export
const
DISPLAY_MODE_EXPANDED
=
'
expanded
'
;
app/constants/constantDisplayMode.ts
0 → 100644
View file @
04db33a6
export
enum
DisplayMode
{
Condensed
=
'
condensed
'
,
Expanded
=
'
expanded
'
}
export
enum
SortBy
{
Alphabetical
=
'
alphabetical
'
,
Activity
=
'
activity
'
}
app/containers/ActionSheet/Button.ts
View file @
04db33a6
import
React
from
'
react
'
;
import
{
TouchableOpacity
}
from
'
react-native
'
;
import
{
isAndroid
}
from
'
../../utils/deviceInfo
'
;
import
Touch
from
'
../../utils/touch
'
;
// Taken from https://github.com/rgommezz/react-native-scroll-bottom-sheet#touchables
export
const
Button
=
isAndroid
?
Touch
:
TouchableOpacity
;
export
const
Button
:
typeof
React
.
Component
=
isAndroid
?
Touch
:
TouchableOpacity
;
app/containers/Avatar/Avatar.tsx
View file @
04db33a6
...
...
@@ -5,6 +5,7 @@ import Touchable from 'react-native-platform-touchable';
import
{
settings
as
RocketChatSettings
}
from
'
@rocket.chat/sdk
'
;
import
{
avatarURL
}
from
'
../../utils/avatar
'
;
import
{
SubscriptionType
}
from
'
../../definitions/ISubscription
'
;
import
Emoji
from
'
../markdown/Emoji
'
;
import
{
IAvatar
}
from
'
./interfaces
'
;
...
...
@@ -27,8 +28,8 @@ const Avatar = React.memo(
text
,
size
=
25
,
borderRadius
=
4
,
type
=
'
d
'
}:
Partial
<
IAvatar
>
)
=>
{
type
=
SubscriptionType
.
DIRECT
}:
IAvatar
)
=>
{
if
((
!
text
&&
!
avatar
&&
!
emoji
&&
!
rid
)
||
!
server
)
{
return
null
;
}
...
...
app/containers/Avatar/index.tsx
View file @
04db33a6
...
...
@@ -7,17 +7,17 @@ import { getUserSelector } from '../../selectors/login';
import
Avatar
from
'
./Avatar
'
;
import
{
IAvatar
}
from
'
./interfaces
'
;
class
AvatarContainer
extends
React
.
Component
<
Partial
<
IAvatar
>
,
any
>
{
class
AvatarContainer
extends
React
.
Component
<
IAvatar
,
any
>
{
private
mounted
:
boolean
;
private
subscription
!
:
any
;
private
subscription
:
any
;
static
defaultProps
=
{
text
:
''
,
type
:
'
d
'
};
constructor
(
props
:
Partial
<
IAvatar
>
)
{
constructor
(
props
:
IAvatar
)
{
super
(
props
);
this
.
mounted
=
false
;
this
.
state
=
{
avatarETag
:
''
};
...
...
@@ -55,7 +55,7 @@ class AvatarContainer extends React.Component<Partial<IAvatar>, any> {
try
{
if
(
this
.
isDirect
)
{
const
{
text
}
=
this
.
props
;
const
[
user
]
=
await
usersCollection
.
query
(
Q
.
where
(
'
username
'
,
text
!
)).
fetch
();
const
[
user
]
=
await
usersCollection
.
query
(
Q
.
where
(
'
username
'
,
text
)).
fetch
();
record
=
user
;
}
else
{
const
{
rid
}
=
this
.
props
;
...
...
@@ -82,7 +82,7 @@ class AvatarContainer extends React.Component<Partial<IAvatar>, any> {
render
()
{
const
{
avatarETag
}
=
this
.
state
;
const
{
serverVersion
}
=
this
.
props
;
return
<
Avatar
avatarETag
=
{
avatarETag
}
serverVersion
=
{
serverVersion
}
{
...
this
.
props
}
/>;
return
<
Avatar
{
...
this
.
props
}
avatarETag
=
{
avatarETag
}
serverVersion
=
{
serverVersion
}
/>;
}
}
...
...
app/containers/Avatar/interfaces.ts
View file @
04db33a6
export
interface
IAvatar
{
server
:
string
;
style
:
any
;
server
?
:
string
;
style
?
:
any
;
text
:
string
;
avatar
:
string
;
emoji
:
string
;
size
:
number
;
borderRadius
:
number
;
type
:
string
;
children
:
JSX
.
Element
;
user
:
{
id
:
string
;
token
:
string
;
avatar
?
:
string
;
emoji
?
:
string
;
size
?
:
number
;
borderRadius
?
:
number
;
type
?
:
string
;
children
?
:
JSX
.
Element
;
user
?
:
{
id
?
:
string
;
token
?
:
string
;
};
theme
:
string
;
onPress
():
void
;
getCustomEmoji
():
any
;
avatarETag
:
string
;
isStatic
:
boolean
|
string
;
rid
:
string
;
blockUnauthenticatedAccess
:
boolean
;
serverVersion
:
string
;
theme
?
:
string
;
onPress
?:
()
=>
void
;
getCustomEmoji
?:
()
=>
any
;
avatarETag
?
:
string
;
isStatic
?
:
boolean
|
string
;
rid
?
:
string
;
blockUnauthenticatedAccess
?
:
boolean
;
serverVersion
?
:
string
;
}
app/containers/BackgroundContainer/index.tsx
View file @
04db33a6
...
...
@@ -6,9 +6,9 @@ import sharedStyles from '../../views/Styles';
import
{
themes
}
from
'
../../constants/colors
'
;
interface
IBackgroundContainer
{
text
:
string
;
theme
:
string
;
loading
:
boolean
;
text
?
:
string
;
theme
?
:
string
;
loading
?
:
boolean
;
}
const
styles
=
StyleSheet
.
create
({
...
...
@@ -35,8 +35,8 @@ const styles = StyleSheet.create({
const
BackgroundContainer
=
({
theme
,
text
,
loading
}:
IBackgroundContainer
)
=>
(
<
View
style
=
{
styles
.
container
}
>
<
ImageBackground
source
=
{
{
uri
:
`message_empty_
${
theme
}
`
}
}
style
=
{
styles
.
image
}
/>
{
text
?
<
Text
style
=
{
[
styles
.
text
,
{
color
:
themes
[
theme
].
auxiliaryTintColor
}]
}
>
{
text
}
</
Text
>
:
null
}
{
loading
?
<
ActivityIndicator
style
=
{
styles
.
text
}
color
=
{
themes
[
theme
].
auxiliaryTintColor
}
/>
:
null
}
{
text
?
<
Text
style
=
{
[
styles
.
text
,
{
color
:
themes
[
theme
!
].
auxiliaryTintColor
}]
}
>
{
text
}
</
Text
>
:
null
}
{
loading
?
<
ActivityIndicator
style
=
{
styles
.
text
}
color
=
{
themes
[
theme
!
].
auxiliaryTintColor
}
/>
:
null
}
</
View
>
);
...
...
app/containers/HeaderButton/Common.tsx
View file @
04db33a6
...
...
@@ -29,9 +29,9 @@ export const CloseModal = React.memo(
export
const
CancelModal
=
React
.
memo
(({
onPress
,
testID
}:
Partial
<
IHeaderButtonCommon
>
)
=>
(
<
Container
left
>
{
isIOS
?
(
<
Item
title
=
{
I18n
.
t
(
'
Cancel
'
)
}
onPress
=
{
onPress
}
testID
=
{
testID
}
/>
<
Item
title
=
{
I18n
.
t
(
'
Cancel
'
)
}
onPress
=
{
onPress
!
}
testID
=
{
testID
}
/>
)
:
(
<
Item
iconName
=
'close'
onPress
=
{
onPress
}
testID
=
{
testID
}
/>
<
Item
iconName
=
'close'
onPress
=
{
onPress
!
}
testID
=
{
testID
}
/>
)
}
</
Container
>
));
...
...
@@ -39,19 +39,19 @@ export const CancelModal = React.memo(({ onPress, testID }: Partial<IHeaderButto
// Right
export
const
More
=
React
.
memo
(({
onPress
,
testID
}:
Partial
<
IHeaderButtonCommon
>
)
=>
(
<
Container
>
<
Item
iconName
=
'kebab'
onPress
=
{
onPress
}
testID
=
{
testID
}
/>
<
Item
iconName
=
'kebab'
onPress
=
{
onPress
!
}
testID
=
{
testID
}
/>
</
Container
>
));
export
const
Download
=
React
.
memo
(({
onPress
,
testID
,
...
props
}:
Partial
<
IHeaderButtonCommon
>
)
=>
(
<
Container
>
<
Item
iconName
=
'download'
onPress
=
{
onPress
}
testID
=
{
testID
}
{
...
props
}
/>
<
Item
iconName
=
'download'
onPress
=
{
onPress
!
}
testID
=
{
testID
}
{
...
props
}
/>
</
Container
>
));
export
const
Preferences
=
React
.
memo
(({
onPress
,
testID
,
...
props
}:
Partial
<
IHeaderButtonCommon
>
)
=>
(
<
Container
>
<
Item
iconName
=
'settings'
onPress
=
{
onPress
}
testID
=
{
testID
}
{
...
props
}
/>
<
Item
iconName
=
'settings'
onPress
=
{
onPress
!
}
testID
=
{
testID
}
{
...
props
}
/>
</
Container
>
));
...
...
app/containers/HeaderButton/HeaderButtonItem.tsx
View file @
04db33a6
...
...
@@ -8,12 +8,12 @@ import { themes } from '../../constants/colors';
import
sharedStyles
from
'
../../views/Styles
'
;
interface
IHeaderButtonItem
{
title
:
string
;
iconName
:
string
;
onPress
():
void
;
testID
:
string
;
theme
:
string
;
badge
():
void
;
title
?
:
string
;
iconName
?
:
string
;
onPress
:
<
T
>
(arg: T) =>
void;
testID
?
: string;
theme
?
: string;
badge
?
(): void;
}
export const BUTTON_HIT_SLOP =
{
...
...
@@ -44,9 +44,9 @@ const Item = ({ title, iconName, onPress, testID, theme, badge }: IHeaderButtonI
<
Touchable
onPress
=
{
onPress
}
testID
=
{
testID
}
hitSlop
=
{
BUTTON_HIT_SLOP
}
style
=
{
styles
.
container
}
>
<>
{
iconName
?
(
<
CustomIcon
name
=
{
iconName
}
size
=
{
24
}
color
=
{
themes
[
theme
].
headerTintColor
}
/>
<
CustomIcon
name
=
{
iconName
}
size
=
{
24
}
color
=
{
themes
[
theme
!
].
headerTintColor
}
/>
)
:
(
<
Text
style
=
{
[
styles
.
title
,
{
color
:
themes
[
theme
].
headerTintColor
}]
}
>
{
title
}
</
Text
>
<
Text
style
=
{
[
styles
.
title
,
{
color
:
themes
[
theme
!
].
headerTintColor
}]
}
>
{
title
}
</
Text
>
)
}
{
badge
?
badge
()
:
null
}
</>
...
...
app/containers/List/ListContainer.tsx
View file @
04db33a6
...
...
@@ -11,10 +11,10 @@ const styles = StyleSheet.create({
});
interface
IListContainer
{
children
:
JSX
.
Element
;
children
:
React
.
ReactNode
;
testID
?:
string
;
}
const
ListContainer
=
React
.
memo
(({
children
,
...
props
}:
IListContainer
)
=>
(
// @ts-ignore
<
ScrollView
contentContainerStyle
=
{
styles
.
container
}
scrollIndicatorInsets
=
{
{
right
:
1
}
}
// https://github.com/facebook/react-native/issues/26610#issuecomment-539843444
...
...
app/containers/List/ListHeader.tsx
View file @
04db33a6
...
...
@@ -20,13 +20,13 @@ const styles = StyleSheet.create({
interface
IListHeader
{
title
:
string
;
theme
:
string
;
translateTitle
:
boolean
;
theme
?
:
string
;
translateTitle
?
:
boolean
;
}
const
ListHeader
=
React
.
memo
(({
title
,
theme
,
translateTitle
=
true
}:
IListHeader
)
=>
(
<
View
style
=
{
styles
.
container
}
>
<
Text
style
=
{
[
styles
.
title
,
{
color
:
themes
[
theme
].
infoText
}]
}
numberOfLines
=
{
1
}
>
<
Text
style
=
{
[
styles
.
title
,
{
color
:
themes
[
theme
!
].
infoText
}]
}
numberOfLines
=
{
1
}
>
{
translateTitle
?
I18n
.
t
(
title
)
:
title
}
</
Text
>
</
View
>
...
...
app/containers/List/ListIcon.tsx
View file @
04db33a6
import
React
from
'
react
'
;
import
{
Style
Sheet
,
View
}
from
'
react-native
'
;
import
{
Style
Prop
,
StyleSheet
,
View
,
ViewStyle
}
from
'
react-native
'
;
import
{
themes
}
from
'
../../constants/colors
'
;
import
{
CustomIcon
}
from
'
../../lib/Icons
'
;
...
...
@@ -7,11 +7,11 @@ import { withTheme } from '../../theme';
import
{
ICON_SIZE
}
from
'
./constants
'
;
interface
IListIcon
{
theme
:
string
;
theme
?
:
string
;
name
:
string
;
color
:
string
;
style
:
object
;
testID
:
string
;
color
?
:
string
;
style
?
:
StyleProp
<
ViewStyle
>
;
testID
?
:
string
;
}
const
styles
=
StyleSheet
.
create
({
...
...
@@ -23,7 +23,7 @@ const styles = StyleSheet.create({
const
ListIcon
=
React
.
memo
(({
theme
,
name
,
color
,
style
,
testID
}:
IListIcon
)
=>
(
<
View
style
=
{
[
styles
.
icon
,
style
]
}
>
<
CustomIcon
name
=
{
name
}
color
=
{
color
??
themes
[
theme
].
auxiliaryText
}
size
=
{
ICON_SIZE
}
testID
=
{
testID
}
/>
<
CustomIcon
name
=
{
name
}
color
=
{
color
??
themes
[
theme
!
].
auxiliaryText
}
size
=
{
ICON_SIZE
}
testID
=
{
testID
}
/>
</
View
>
));
...
...
Prev
1
2
3
4
5
…
11
Next
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new 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