Commit 2e8a0993 authored by Filipe de Lima Brito's avatar Filipe de Lima Brito

Add register username when log in via OAuth for the first time (and there is no user created yet)

parent ca864422
...@@ -226,7 +226,7 @@ class LoginPresenter @Inject constructor(private val view: LoginView, ...@@ -226,7 +226,7 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
registerPushToken() registerPushToken()
navigator.toChatList() navigator.toChatList()
} else if (loginType == TYPE_LOGIN_OAUTH) { } else if (loginType == TYPE_LOGIN_OAUTH) {
view.alertRequiresUsername() navigator.toRegisterUsername(token.userId, token.authToken)
} }
} catch (exception: RocketChatException) { } catch (exception: RocketChatException) {
when (exception) { when (exception) {
......
...@@ -187,9 +187,4 @@ interface LoginView : LoadingView, MessageView, InternetView, VersionCheckView { ...@@ -187,9 +187,4 @@ interface LoginView : LoadingView, MessageView, InternetView, VersionCheckView {
* Alerts the user about a wrong inputted password. * Alerts the user about a wrong inputted password.
*/ */
fun alertWrongPassword() fun alertWrongPassword()
/**
* Alerts the user about the need of creating an username using the web app when creating an user through OAuth.
*/
fun alertRequiresUsername()
} }
\ No newline at end of file
...@@ -14,7 +14,6 @@ import android.view.ViewGroup ...@@ -14,7 +14,6 @@ import android.view.ViewGroup
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.ScrollView import android.widget.ScrollView
import android.widget.Toast
import chat.rocket.android.BuildConfig import chat.rocket.android.BuildConfig
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.authentication.login.presentation.LoginPresenter import chat.rocket.android.authentication.login.presentation.LoginPresenter
...@@ -292,10 +291,6 @@ class LoginFragment : Fragment(), LoginView { ...@@ -292,10 +291,6 @@ class LoginFragment : Fragment(), LoginView {
text_password.requestFocus() text_password.requestFocus()
} }
override fun alertRequiresUsername() {
showToast(getString(R.string.msg_requires_username), Toast.LENGTH_LONG)
}
override fun alertNotRecommendedVersion() { override fun alertNotRecommendedVersion() {
context?.let { context?.let {
AlertDialog.Builder(it) AlertDialog.Builder(it)
......
...@@ -3,6 +3,7 @@ package chat.rocket.android.authentication.presentation ...@@ -3,6 +3,7 @@ package chat.rocket.android.authentication.presentation
import android.content.Intent import android.content.Intent
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.authentication.login.ui.LoginFragment import chat.rocket.android.authentication.login.ui.LoginFragment
import chat.rocket.android.authentication.registerusername.ui.RegisterUsernameFragment
import chat.rocket.android.authentication.signup.ui.SignupFragment import chat.rocket.android.authentication.signup.ui.SignupFragment
import chat.rocket.android.authentication.twofactor.ui.TwoFAFragment import chat.rocket.android.authentication.twofactor.ui.TwoFAFragment
import chat.rocket.android.authentication.ui.AuthenticationActivity import chat.rocket.android.authentication.ui.AuthenticationActivity
...@@ -37,6 +38,12 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity) { ...@@ -37,6 +38,12 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity) {
activity.overridePendingTransition(R.anim.slide_up, R.anim.hold) activity.overridePendingTransition(R.anim.slide_up, R.anim.hold)
} }
fun toRegisterUsername(userId: String, authToken: String) {
activity.addFragmentBackStack("RegisterUsernameFragment", R.id.fragment_container) {
RegisterUsernameFragment.newInstance(userId, authToken)
}
}
fun toChatList() { fun toChatList() {
activity.startActivity(Intent(activity, MainActivity::class.java)) activity.startActivity(Intent(activity, MainActivity::class.java))
activity.finish() activity.finish()
......
package chat.rocket.android.authentication.registerusername.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.authentication.registerusername.presentation.RegisterUsernameView
import chat.rocket.android.authentication.registerusername.ui.RegisterUsernameFragment
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
@PerFragment
class RegisterUsernameFragmentModule {
@Provides
fun registerUsernameView(frag: RegisterUsernameFragment): RegisterUsernameView {
return frag
}
@Provides
fun provideLifecycleOwner(frag: RegisterUsernameFragment): 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.authentication.registerusername.di
import chat.rocket.android.authentication.registerusername.ui.RegisterUsernameFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class RegisterUsernameFragmentProvider {
@ContributesAndroidInjector(modules = [RegisterUsernameFragmentModule::class])
abstract fun provideRegisterUsernameFragment(): RegisterUsernameFragment
}
\ No newline at end of file
package chat.rocket.android.authentication.registerusername.presentation
import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.extensions.registerPushToken
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.Token
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.updateOwnBasicInformation
import javax.inject.Inject
class RegisterUsernamePresenter @Inject constructor(
private val view: RegisterUsernameView,
private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator,
private val tokenRepository: TokenRepository,
private val localRepository: LocalRepository,
private val factory: RocketChatClientFactory,
private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
serverInteractor: GetCurrentServerInteractor,
settingsInteractor: GetSettingsInteractor
) {
private val currentServer = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(currentServer)
private var settings: PublicSettings = settingsInteractor.get(serverInteractor.get()!!)
fun registerUsername(username: String, userId: String, authToken: String) {
if (username.isBlank()) {
view.alertBlankUsername()
} else {
launchUI(strategy) {
if (NetworkHelper.hasInternetAccess()) {
view.showLoading()
try {
val me = client.updateOwnBasicInformation(username = username)
val registeredUsername = me.username
if (registeredUsername != null) {
saveAccount(registeredUsername)
tokenRepository.save(currentServer, Token(userId, authToken))
registerPushToken()
navigator.toChatList()
}
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
}
} else {
view.showNoInternetConnection()
}
}
}
}
private suspend fun registerPushToken() {
localRepository.get(LocalRepository.KEY_PUSH_TOKEN)?.let {
client.registerPushToken(it, getAccountsInteractor.get(), factory)
}
// TODO: When the push token is null, at some point we should receive it with
// onTokenRefresh() on FirebaseTokenService, we need to confirm it.
}
private suspend fun saveAccount(username: String) {
val icon = settings.favicon()?.let {
UrlHelper.getServerLogoUrl(currentServer, it)
}
val logo = settings.wideTile()?.let {
UrlHelper.getServerLogoUrl(currentServer, it)
}
val thumb = UrlHelper.getAvatarUrl(currentServer, username)
val account = Account(currentServer, icon, logo, username, thumb)
saveAccountInteractor.save(account)
}
}
\ No newline at end of file
package chat.rocket.android.authentication.registerusername.presentation
import chat.rocket.android.core.behaviours.InternetView
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
interface RegisterUsernameView : LoadingView, MessageView, InternetView {
/**
* Alerts the user about a blank username.
*/
fun alertBlankUsername()
}
\ No newline at end of file
package chat.rocket.android.authentication.registerusername.ui
import DrawableHelper
import android.os.Build
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import chat.rocket.android.R
import chat.rocket.android.authentication.registerusername.presentation.RegisterUsernamePresenter
import chat.rocket.android.authentication.registerusername.presentation.RegisterUsernameView
import chat.rocket.android.util.extensions.*
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_register_username.*
import javax.inject.Inject
class RegisterUsernameFragment : Fragment(), RegisterUsernameView {
@Inject lateinit var presenter: RegisterUsernamePresenter
private lateinit var userId: String
private lateinit var authToken: String
companion object {
private const val USER_ID = "user_id"
private const val AUTH_TOKEN = "auth_token"
fun newInstance(userId: String, authToken: String) = RegisterUsernameFragment().apply {
arguments = Bundle(1).apply {
putString(USER_ID, userId)
putString(AUTH_TOKEN, authToken)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
// TODO - research a better way to initialize parameters on fragments.
userId = arguments?.getString(USER_ID) ?: ""
authToken = arguments?.getString(AUTH_TOKEN) ?: ""
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_authentication_register_username)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activity?.apply {
text_username.requestFocus()
showKeyboard(text_username)
}
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
tintEditTextDrawableStart()
}
setupOnClickListener()
}
override fun alertBlankUsername() {
vibrateSmartPhone()
text_username.shake()
}
override fun showLoading() {
disableUserInput()
view_loading.setVisible(true)
}
override fun hideLoading() {
view_loading.setVisible(false)
enableUserInput()
}
override fun showMessage(resId: Int) {
showToast(resId)
}
override fun showMessage(message: String) {
showToast(message)
}
override fun showGenericErrorMessage() {
showMessage(getString(R.string.msg_generic_error))
}
override fun showNoInternetConnection() {
showMessage(getString(R.string.msg_no_internet_connection))
}
private fun tintEditTextDrawableStart() {
activity?.apply {
val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, this)
DrawableHelper.wrapDrawable(atDrawable)
DrawableHelper.tintDrawable(atDrawable, this, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawable(text_username, atDrawable)
}
}
private fun enableUserInput() {
button_use_this_username.isEnabled = true
text_username.isEnabled = true
}
private fun disableUserInput() {
button_use_this_username.isEnabled = false
text_username.isEnabled = true
}
private fun setupOnClickListener() {
button_use_this_username.setOnClickListener {
presenter.registerUsername(text_username.textContent, userId, authToken)
}
}
}
\ No newline at end of file
...@@ -44,7 +44,7 @@ class TwoFAFragment : Fragment(), TwoFAView { ...@@ -44,7 +44,7 @@ class TwoFAFragment : Fragment(), TwoFAView {
password = arguments?.getString(PASSWORD) ?: "" password = arguments?.getString(PASSWORD) ?: ""
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_authentication_two_fa, container, false) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_authentication_two_fa)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
...@@ -58,6 +58,7 @@ class TwoFAFragment : Fragment(), TwoFAView { ...@@ -58,6 +58,7 @@ class TwoFAFragment : Fragment(), TwoFAView {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
tintEditTextDrawableStart() tintEditTextDrawableStart()
} }
setupOnClickListener() setupOnClickListener()
} }
......
...@@ -2,6 +2,7 @@ package chat.rocket.android.dagger.module ...@@ -2,6 +2,7 @@ package chat.rocket.android.dagger.module
import chat.rocket.android.authentication.di.AuthenticationModule import chat.rocket.android.authentication.di.AuthenticationModule
import chat.rocket.android.authentication.login.di.LoginFragmentProvider import chat.rocket.android.authentication.login.di.LoginFragmentProvider
import chat.rocket.android.authentication.registerusername.di.RegisterUsernameFragmentProvider
import chat.rocket.android.authentication.server.di.ServerFragmentProvider import chat.rocket.android.authentication.server.di.ServerFragmentProvider
import chat.rocket.android.authentication.signup.di.SignupFragmentProvider import chat.rocket.android.authentication.signup.di.SignupFragmentProvider
import chat.rocket.android.authentication.twofactor.di.TwoFAFragmentProvider import chat.rocket.android.authentication.twofactor.di.TwoFAFragmentProvider
...@@ -30,6 +31,7 @@ abstract class ActivityBuilder { ...@@ -30,6 +31,7 @@ abstract class ActivityBuilder {
@ContributesAndroidInjector(modules = [AuthenticationModule::class, @ContributesAndroidInjector(modules = [AuthenticationModule::class,
ServerFragmentProvider::class, ServerFragmentProvider::class,
LoginFragmentProvider::class, LoginFragmentProvider::class,
RegisterUsernameFragmentProvider::class,
SignupFragmentProvider::class, SignupFragmentProvider::class,
TwoFAFragmentProvider::class TwoFAFragmentProvider::class
]) ])
......
...@@ -55,6 +55,11 @@ fun Activity.hideKeyboard() { ...@@ -55,6 +55,11 @@ fun Activity.hideKeyboard() {
} }
} }
fun Activity.showKeyboard(view: View) {
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(view, InputMethodManager.RESULT_UNCHANGED_SHOWN)
}
fun Activity.showToast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT) = fun Activity.showToast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT) =
showToast(getString(resource), duration) showToast(getString(resource), duration)
......
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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="match_parent"
tools:context=".authentication.registerusername.ui.RegisterUsernameFragment">
<TextView
android:id="@+id/text_headline"
style="@style/Authentication.Headline.TextView"
android:layout_centerHorizontal="true"
android:text="@string/title_register_username" />
<EditText
android:id="@+id/text_username"
style="@style/Authentication.EditText"
android:layout_below="@id/text_headline"
android:layout_marginTop="32dp"
android:drawableStart="@drawable/ic_at_black_24dp"
android:hint="@string/msg_username"
android:imeOptions="actionDone"
android:inputType="text" />
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:visibility="gone"
app:indicatorName="BallPulseIndicator"
tools:visibility="visible" />
<Button
android:id="@+id/button_use_this_username"
style="@style/Authentication.Button"
android:layout_alignParentBottom="true"
android:text="@string/action_use_this_username" />
</RelativeLayout>
\ No newline at end of file
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
<!-- Titles --> <!-- Titles -->
<string name="title_sign_in_your_server">अपने सर्वर में साइन इन करें</string> <string name="title_sign_in_your_server">अपने सर्वर में साइन इन करें</string>
<string name="title_log_in">लॉग इन करें</string> <string name="title_log_in">लॉग इन करें</string>
<string name="title_register_username">रजिस्टर उपयोगकर्ता नाम</string>
<string name="title_sign_up">साइन अप करें</string> <string name="title_sign_up">साइन अप करें</string>
<string name="title_authentication">प्रमाणीकरण</string> <string name="title_authentication">प्रमाणीकरण</string>
<string name="title_legal_terms">कानूनी शर्तें</string> <string name="title_legal_terms">कानूनी शर्तें</string>
...@@ -15,6 +16,7 @@ ...@@ -15,6 +16,7 @@
<!-- Actions --> <!-- Actions -->
<string name="action_connect">जुडिये</string> <string name="action_connect">जुडिये</string>
<string name="action_use_this_username">इस उपयोगकर्ता नाम का उपयोग करें</string>
<string name="action_login_or_sign_up">लॉग इन करने या खाता बनाने के लिए इस बटन को टैप करें</string> <string name="action_login_or_sign_up">लॉग इन करने या खाता बनाने के लिए इस बटन को टैप करें</string>
<string name="action_terms_of_service">सेवा की शर्तें</string> <string name="action_terms_of_service">सेवा की शर्तें</string>
<string name="action_privacy_policy">गोपनीयता नीति</string> <string name="action_privacy_policy">गोपनीयता नीति</string>
...@@ -73,7 +75,6 @@ ...@@ -73,7 +75,6 @@
<string name="msg_preview_photo">तस्वीरें</string> <string name="msg_preview_photo">तस्वीरें</string>
<string name="msg_unread_messages">अपठित संदेश</string> <string name="msg_unread_messages">अपठित संदेश</string>
<string name="msg_no_messages_yet">अभी तक कोई पोस्ट नहीं</string> <string name="msg_no_messages_yet">अभी तक कोई पोस्ट नहीं</string>
<string name="msg_requires_username">उपयोगकर्ता नाम आवश्यक है: कृपया, वेब संस्करण के माध्यम से एक उपयोगकर्ता नाम बनाएं और लॉग इन करने के लिए वापस आएँ</string>
<string name="msg_ok">OK</string> <string name="msg_ok">OK</string>
<string name="msg_ver_not_recommended"> <string name="msg_ver_not_recommended">
ऐसा लगता है कि आपका सर्वर संस्करण अनुशंसित संस्करण %1$s के नीचे है।\nआप अभी भी लॉगिन कर सकते हैं लेकिन आप अप्रत्याशित व्यवहार का अनुभव कर सकते हैं ऐसा लगता है कि आपका सर्वर संस्करण अनुशंसित संस्करण %1$s के नीचे है।\nआप अभी भी लॉगिन कर सकते हैं लेकिन आप अप्रत्याशित व्यवहार का अनुभव कर सकते हैं
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
<!-- Titles --> <!-- Titles -->
<string name="title_sign_in_your_server">Faça login no seu servidor</string> <string name="title_sign_in_your_server">Faça login no seu servidor</string>
<string name="title_log_in">Entrar</string> <string name="title_log_in">Entrar</string>
<string name="title_register_username">Registre o nome de usuário</string>
<string name="title_sign_up">Inscreva-se</string> <string name="title_sign_up">Inscreva-se</string>
<string name="title_authentication">Autenticação</string> <string name="title_authentication">Autenticação</string>
<string name="title_legal_terms">Termos Legais</string> <string name="title_legal_terms">Termos Legais</string>
...@@ -15,6 +16,7 @@ ...@@ -15,6 +16,7 @@
<!-- Actions --> <!-- Actions -->
<string name="action_connect">Conectar</string> <string name="action_connect">Conectar</string>
<string name="action_use_this_username">Usar este nome de usuário</string>
<string name="action_login_or_sign_up">Toque este botão para fazer login ou criar uma conta</string> <string name="action_login_or_sign_up">Toque este botão para fazer login ou criar uma conta</string>
<string name="action_terms_of_service">Termos de Serviço</string> <string name="action_terms_of_service">Termos de Serviço</string>
<string name="action_privacy_policy">Política de Privacidade</string> <string name="action_privacy_policy">Política de Privacidade</string>
...@@ -73,7 +75,6 @@ ...@@ -73,7 +75,6 @@
<string name="msg_preview_audio">Audio</string> <string name="msg_preview_audio">Audio</string>
<string name="msg_preview_photo">Foto</string> <string name="msg_preview_photo">Foto</string>
<string name="msg_no_messages_yet">Nenhuma mensagem ainda</string> <string name="msg_no_messages_yet">Nenhuma mensagem ainda</string>
<string name="msg_requires_username">Nome de usuário requerido: Por favor, crie um nome de usuário através da versão web e volte para fazer login.</string>
<string name="msg_ok">OK</string> <string name="msg_ok">OK</string>
<string name="msg_ver_not_recommended"> <string name="msg_ver_not_recommended">
Parece que a versão do seu servidor está abaixo da recomendada %1$s.\nVocê ainda assim pode logar e continuar mas podem ocorrer alguns problemas inesperados. Parece que a versão do seu servidor está abaixo da recomendada %1$s.\nVocê ainda assim pode logar e continuar mas podem ocorrer alguns problemas inesperados.
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
<!-- Titles --> <!-- Titles -->
<string name="title_sign_in_your_server">Sign in your server</string> <string name="title_sign_in_your_server">Sign in your server</string>
<string name="title_log_in">Log in</string> <string name="title_log_in">Log in</string>
<string name="title_register_username">Register username</string>
<string name="title_sign_up">Sign up</string> <string name="title_sign_up">Sign up</string>
<string name="title_authentication">Authentication</string> <string name="title_authentication">Authentication</string>
<string name="title_legal_terms">Legal Terms</string> <string name="title_legal_terms">Legal Terms</string>
...@@ -16,6 +17,7 @@ ...@@ -16,6 +17,7 @@
<!-- Actions --> <!-- Actions -->
<string name="action_connect">Connect</string> <string name="action_connect">Connect</string>
<string name="action_use_this_username">Use this username</string>
<string name="action_login_or_sign_up">Tap this button to log in or create an account</string> <string name="action_login_or_sign_up">Tap this button to log in or create an account</string>
<string name="action_terms_of_service">Terms of Service</string> <string name="action_terms_of_service">Terms of Service</string>
<string name="action_privacy_policy">Privacy Policy</string> <string name="action_privacy_policy">Privacy Policy</string>
...@@ -75,7 +77,6 @@ ...@@ -75,7 +77,6 @@
<string name="msg_preview_audio">Audio</string> <string name="msg_preview_audio">Audio</string>
<string name="msg_preview_photo">Photo</string> <string name="msg_preview_photo">Photo</string>
<string name="msg_no_messages_yet">No messages yet</string> <string name="msg_no_messages_yet">No messages yet</string>
<string name="msg_requires_username">Username required: Please, create an username through the web version and come back to log in.</string>
<string name="msg_ok">OK</string> <string name="msg_ok">OK</string>
<string name="msg_ver_not_recommended"> <string name="msg_ver_not_recommended">
Looks like your server version is below the recommended version %1$s.\nYou can still login but you may experience unexpected behaviors.</string> Looks like your server version is below the recommended version %1$s.\nYou can still login but you may experience unexpected behaviors.</string>
......
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