Unverified Commit b8e10260 authored by Filipe de Lima Brito's avatar Filipe de Lima Brito Committed by GitHub

Merge pull request #787 from samrmur/develop-2.x

[NEW] Password Update Feature - Issue #755
parents e0d61fe1 56f1be87
......@@ -55,6 +55,10 @@
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" />
<activity
android:name=".settings.password.ui.PasswordActivity"
android:theme="@style/AppTheme" />
<receiver
android:name="com.google.android.gms.gcm.GcmReceiver"
android:exported="true"
......
......@@ -17,6 +17,8 @@ import chat.rocket.android.main.di.MainActivityProvider
import chat.rocket.android.main.di.MainModule
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.profile.di.ProfileFragmentProvider
import chat.rocket.android.settings.password.di.PasswordFragmentProvider
import chat.rocket.android.settings.password.ui.PasswordActivity
import dagger.Module
import dagger.android.ContributesAndroidInjector
......@@ -48,4 +50,8 @@ abstract class ActivityBuilder {
@PerActivity
@ContributesAndroidInjector(modules = [PinnedMessagesFragmentProvider::class])
abstract fun bindPinnedMessagesActivity(): PinnedMessagesActivity
@PerActivity
@ContributesAndroidInjector(modules = [PasswordFragmentProvider::class])
abstract fun bindPasswordActivity(): PasswordActivity
}
\ No newline at end of file
......@@ -5,6 +5,7 @@ import chat.rocket.android.R
import chat.rocket.android.chatrooms.ui.ChatRoomsFragment
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.profile.ui.ProfileFragment
import chat.rocket.android.settings.ui.SettingsFragment
import chat.rocket.android.util.extensions.addFragment
class MainNavigator(internal val activity: MainActivity, internal val context: Context) {
......@@ -20,4 +21,10 @@ class MainNavigator(internal val activity: MainActivity, internal val context: C
ProfileFragment.newInstance()
}
}
fun toSettings() {
activity.addFragment("SettingsFragment", R.id.fragment_container) {
SettingsFragment.newInstance()
}
}
}
\ No newline at end of file
......@@ -21,6 +21,8 @@ class MainPresenter @Inject constructor(private val navigator: MainNavigator,
fun toUserProfile() = navigator.toUserProfile()
fun toSettings() = navigator.toSettings()
/**
* Logout from current server.
*/
......
......@@ -7,10 +7,16 @@ import android.support.v7.app.AppCompatActivity
import android.view.Gravity
import android.view.MenuItem
import chat.rocket.android.R
import chat.rocket.android.chatrooms.ui.ChatRoomsFragment
import chat.rocket.android.profile.ui.ProfileFragment
import chat.rocket.android.settings.ui.SettingsFragment
import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.main.presentation.MainPresenter
import chat.rocket.android.main.presentation.MainView
import chat.rocket.android.util.extensions.showToast
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
......@@ -81,6 +87,9 @@ class MainActivity : AppCompatActivity(), MainView, HasSupportFragmentInjector {
R.id.action_profile -> {
presenter.toUserProfile()
}
R.id.action_settings -> {
presenter.toSettings()
}
R.id.action_logout -> {
presenter.logout()
}
......
......@@ -50,8 +50,9 @@ class ProfilePresenter @Inject constructor (private val view: ProfileView,
launchUI(strategy) {
view.showLoading()
try {
if(avatarUrl!="")
if(avatarUrl!="") {
client.setAvatar(avatarUrl)
}
val user = client.updateProfile(myselfId, email, name, username)
view.showProfileUpdateSuccessfullyMessage()
loadUserProfile()
......@@ -59,8 +60,8 @@ class ProfilePresenter @Inject constructor (private val view: ProfileView,
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
}
......
package chat.rocket.android.settings.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.settings.presentation.SettingsView
import chat.rocket.android.settings.ui.SettingsFragment
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
@PerFragment
class SettingsFragmentModule {
@Provides
fun settingsView(frag: SettingsFragment): SettingsView {
return frag
}
@Provides
fun settingsLifecycleOwner(frag: SettingsFragment): LifecycleOwner {
return frag
}
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
}
\ No newline at end of file
package chat.rocket.android.settings.di
import chat.rocket.android.settings.ui.SettingsFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class SettingsFragmentProvider {
@ContributesAndroidInjector(modules = [SettingsFragmentModule::class])
abstract fun provideSettingsFragment(): SettingsFragment
}
\ No newline at end of file
package chat.rocket.android.settings.password.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.settings.password.presentation.PasswordView
import chat.rocket.android.settings.password.ui.PasswordFragment
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
@PerFragment
class PasswordFragmentModule {
@Provides
fun passwordView(frag: PasswordFragment): PasswordView {
return frag
}
@Provides
fun settingsLifecycleOwner(frag: PasswordFragment): LifecycleOwner {
return frag
}
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
}
package chat.rocket.android.settings.password.di
import chat.rocket.android.settings.password.ui.PasswordFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class PasswordFragmentProvider {
@ContributesAndroidInjector(modules = [PasswordFragmentModule::class])
abstract fun providePasswordFragment(): PasswordFragment
}
\ No newline at end of file
package chat.rocket.android.settings.password.presentation
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.updateProfile
import javax.inject.Inject
class PasswordPresenter @Inject constructor (private val view: PasswordView,
private val strategy: CancelStrategy,
serverInteractor: GetCurrentServerInteractor,
factory: RocketChatClientFactory){
private val serverUrl = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(serverUrl)
fun updatePassword(password: String) {
launchUI(strategy) {
try {
view.showLoading()
client.updateProfile(client.me().id, null, null, password, null)
view.showPasswordSuccessfullyUpdatedMessage()
view.hideLoading()
} catch (exception: RocketChatException) {
view.showPasswordFailsUpdateMessage(exception.message)
view.hideLoading()
}
}
}
}
\ No newline at end of file
package chat.rocket.android.settings.password.presentation
import chat.rocket.android.core.behaviours.LoadingView
interface PasswordView: LoadingView {
/**
* Shows a message when a user's password is successfully updated
*/
fun showPasswordSuccessfullyUpdatedMessage()
/**
* Shows a message when the user's password fails to update
* @param error is a String containing the failure message
*/
fun showPasswordFailsUpdateMessage(error : String?)
}
package chat.rocket.android.settings.password.ui
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import chat.rocket.android.R
import chat.rocket.android.chatrooms.ui.ChatRoomsFragment
import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.util.extensions.textContent
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.app_bar_password.*
import javax.inject.Inject
class PasswordActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_password)
setupToolbar()
addFragment("PasswordFragment")
}
override fun onBackPressed() {
super.onBackPressed()
finish()
overridePendingTransition(R.anim.close_enter, R.anim.close_exit)
}
override fun onSupportNavigateUp(): Boolean {
onBackPressed()
return super.onNavigateUp()
}
override fun supportFragmentInjector(): AndroidInjector<Fragment> = fragmentDispatchingAndroidInjector
private fun addFragment(tag: String) {
addFragment(tag, R.id.fragment_container) {
PasswordFragment.newInstance()
}
}
private fun setupToolbar() {
setSupportActionBar(toolbar)
text_change_password.textContent = resources.getString(R.string.title_password)
}
}
package chat.rocket.android.settings.password.ui
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.*
import android.widget.Toast
import chat.rocket.android.R
import chat.rocket.android.settings.password.presentation.PasswordPresenter
import chat.rocket.android.settings.password.presentation.PasswordView
import chat.rocket.android.util.extensions.asObservable
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.textContent
import android.support.v7.view.ActionMode
import dagger.android.support.AndroidSupportInjection
import io.reactivex.rxkotlin.Observables
import kotlinx.android.synthetic.main.fragment_password.*
import javax.inject.Inject
class PasswordFragment: Fragment(), PasswordView, android.support.v7.view.ActionMode.Callback {
@Inject lateinit var presenter: PasswordPresenter
private var actionMode: ActionMode? = null
companion object {
fun newInstance() = PasswordFragment()
}
override fun onCreate(savedInstanceState: Bundle?) {
AndroidSupportInjection.inject(this)
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_password)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
listenToChanges()
}
override fun hideLoading() {
layout_new_password.visibility = View.VISIBLE
layout_confirm_password.visibility = View.VISIBLE
view_loading.visibility = View.GONE
}
override fun onActionItemClicked(mode: ActionMode, menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_password -> {
presenter.updatePassword(text_new_password.textContent)
mode.finish()
return true
}
else -> {
false
}
}
}
override fun onCreateActionMode(mode: ActionMode, menu: Menu?): Boolean {
mode.menuInflater.inflate(R.menu.password, menu)
mode.title = resources.getString(R.string.action_confirm_password)
return true
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean = false
override fun onDestroyActionMode(mode: ActionMode?) {
actionMode = null
}
override fun showLoading() {
layout_new_password.visibility = View.GONE
layout_confirm_password.visibility = View.GONE
view_loading.visibility = View.VISIBLE
}
override fun showPasswordFailsUpdateMessage(error: String?) {
showToast("Password fails to update: " + error)
}
override fun showPasswordSuccessfullyUpdatedMessage() {
showToast("Password was successfully updated!")
}
private fun finishActionMode() = actionMode?.finish()
private fun listenToChanges() {
Observables.combineLatest(text_new_password.asObservable(), text_confirm_password.asObservable()).subscribe {
val textPassword = text_new_password.textContent
val textConfirmPassword = text_confirm_password.textContent
if (textPassword.length > 5 && textConfirmPassword.length > 5 && textPassword.equals(textConfirmPassword))
startActionMode()
else
finishActionMode()
}
}
private fun showToast(msg: String?) {
Toast.makeText(context, msg, Toast.LENGTH_LONG).show()
}
private fun startActionMode() {
if (actionMode == null) {
actionMode = (activity as PasswordActivity).startSupportActionMode(this)
}
}
}
\ No newline at end of file
package chat.rocket.android.settings.presentation
interface SettingsView
package chat.rocket.android.settings.ui
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import chat.rocket.android.R
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.settings.password.ui.PasswordActivity
import chat.rocket.android.settings.presentation.SettingsView
import chat.rocket.android.util.extensions.inflate
import kotlinx.android.synthetic.main.app_bar.*
import kotlinx.android.synthetic.main.fragment_settings.*
import kotlin.reflect.KClass
class SettingsFragment: Fragment(), SettingsView, AdapterView.OnItemClickListener {
companion object {
fun newInstance() = SettingsFragment()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_settings)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar()
setupListView()
}
override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
when (parent?.getItemAtPosition(position).toString()) {
"Change Password" -> {
startNewActivity(PasswordActivity::class)
}
}
}
private fun setupListView() {
settings_list.onItemClickListener = this
}
private fun setupToolbar() {
(activity as MainActivity).toolbar.title = getString(R.string.title_settings)
}
private fun startNewActivity(classType: KClass<out AppCompatActivity>) {
startActivity(Intent(activity, classType.java))
activity?.overridePendingTransition(R.anim.open_enter, R.anim.open_exit)
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:theme="@style/AppTheme"
tools:context=".settings.password.ui.PasswordActivity">
<include
android:id="@+id/layout_app_bar"
layout="@layout/app_bar_password" />
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.AppBarLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways"
app:navigationIcon="?android:attr/homeAsUpIndicator"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:theme="@style/ActionModeStyle">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/text_change_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold"
tools:text="@string/title_password" />
</RelativeLayout>
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<android.support.design.widget.TextInputLayout
android:id="@+id/layout_new_password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:theme="@style/EditText.Password"
app:layout_constraintBottom_toTopOf="@id/middle_guide"
app:layout_constraintStart_toEndOf="@id/start_guide"
app:layout_constraintEnd_toStartOf="@id/end_guide">
<EditText
android:id="@+id/text_new_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/msg_new_password"
android:inputType="textPassword" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:id="@+id/layout_confirm_password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:theme="@style/EditText.Password"
app:layout_constraintTop_toBottomOf="@id/middle_guide"
app:layout_constraintStart_toEndOf="@id/start_guide"
app:layout_constraintEnd_toStartOf="@id/end_guide">
<EditText
android:id="@+id/text_confirm_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/msg_confirm_password"
android:inputType="textPassword" />
</android.support.design.widget.TextInputLayout>
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/middle_guide"
app:layout_constraintStart_toEndOf="@id/start_guide"
app:layout_constraintEnd_toStartOf="@id/end_guide"
app:indicatorColor="@color/black"
app:indicatorName="BallPulseIndicator" />
<android.support.constraint.Guideline
android:id="@+id/middle_guide"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.2" />
<android.support.constraint.Guideline
android:id="@+id/start_guide"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.05" />
<android.support.constraint.Guideline
android:id="@+id/end_guide"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.95" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/settings_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:entries="@array/settings_actions"/>
</RelativeLayout>
\ No newline at end of file
......@@ -15,17 +15,16 @@
android:id="@+id/action_profile"
android:icon="@drawable/ic_person_black_24dp"
android:title="@string/title_profile" />
</group>
<group
android:id="@+id/menu_section_2"
android:checkableBehavior="none">
<item
android:id="@+id/action_settings"
android:icon="@drawable/ic_settings_black_24dp"
android:title="@string/action_settings" />
android:title="@string/title_settings"/>
</group>
<group
android:id="@+id/menu_section_2"
android:checkableBehavior="none">
<item
android:id="@+id/action_logout"
android:icon="@drawable/ic_exit_to_app_black_24dp"
......
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_password"
android:icon="@drawable/ic_check_white_24dp"
android:title="@string/action_confirm_password" />
</menu>
\ No newline at end of file
......@@ -7,6 +7,8 @@
<string name="title_legal_terms">Termos Legais</string>
<string name="title_chats">Chats</string>
<string name="title_profile">Perfil</string>
<string name="title_settings">Configurações</string>
<string name="title_password">Alterar senha</string>
<string name="title_update_profile">Editar perfil</string>
<!-- Actions -->
......@@ -18,6 +20,12 @@
<string name="action_settings">Configurações</string>
<string name="action_logout">Sair</string>
<string name="action_files">Arquivos</string>
<string name="action_confirm_password">Confirme a nova senha</string>
<!-- Settings List -->
<string-array name="settings_actions">
<item name="item_password">Alterar senha</item>
</string-array>
<!-- Regular information messages -->
<string name="msg_no_internet_connection">Sem conexão à internet</string>
......@@ -51,6 +59,8 @@
<string name="msg_content_description_show_attachment_options">Mostrar opções de anexo</string>
<string name="msg_you">Você</string>
<string name="msg_unknown">Desconhecido</string>
<string name="msg_new_password">Informe a nova senha</string>
<string name="msg_confirm_password">Confirme a nova senha</string>
<!-- System messages -->
<string name="message_room_name_changed">Nome da sala alterado para: %1$s por %2$s</string>
......
......@@ -8,6 +8,8 @@
<string name="title_legal_terms">Legal Terms</string>
<string name="title_chats">Chats</string>
<string name="title_profile">Profile</string>
<string name="title_settings">Settings</string>
<string name="title_password">Change Password</string>
<string name="title_update_profile">Update profile</string>
<!-- Actions -->
......@@ -19,6 +21,12 @@
<string name="action_settings">Settings</string>
<string name="action_logout">Logout</string>
<string name="action_files">Files</string>
<string name="action_confirm_password">Confirm Password Change</string>
<!-- Settings List -->
<string-array name="settings_actions">
<item name="item_password">Change Password</item>
</string-array>
<!-- Regular information messages -->
<string name="msg_no_internet_connection">No internet connection</string>
......@@ -53,6 +61,8 @@
<string name="msg_content_description_show_attachment_options">Show attachment options</string>
<string name="msg_you">You</string>
<string name="msg_unknown">Unknown</string>
<string name="msg_new_password">Enter New Password</string>
<string name="msg_confirm_password">Confirm New Password</string>
<!-- System messages -->
<string name="message_room_name_changed">Room name changed to: %1$s by %2$s</string>
......
......@@ -56,6 +56,21 @@
<item name="android:background">@drawable/effect_ripple</item>
</style>
<style name="EditText.Password" parent="TextAppearance.AppCompat">
<!-- Hint color and label color in FALSE state -->
<item name="android:textColorHint">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorPrimaryDark</item>
<item name="colorControlNormal">@color/colorPrimaryDark</item>
<item name="colorControlActivated">@color/colorPrimaryDark</item>
</style>
<style name="AuthenticationLabel" parent="TextAppearance.AppCompat.Medium">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">50dp</item>
<item name="android:layout_marginStart">@dimen/screen_edge_left_and_right_margins</item>
<item name="android:paddingStart">@dimen/edit_text_margin</item>
</style>
<style name="ChatRoom.Name.TextView" parent="TextAppearance.AppCompat.Title">
<item name="android:ellipsize">end</item>
<item name="android:maxLines">1</item>
......@@ -81,4 +96,9 @@
<item name="android:background">@drawable/style_edit_text_profile</item>
</style>
<style name="ActionModeStyle" parent="ThemeOverlay.AppCompat.Dark.ActionBar">
<item name="android:actionModeCloseDrawable">@drawable/ic_close_white_24dp</item>
<item name="actionModeCloseDrawable">@drawable/ic_close_white_24dp</item>
</style>
</resources>
\ No newline at end of file
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