Commit c0462fe1 authored by Leonardo Aramaki's avatar Leonardo Aramaki

Merge branch 'develop-2.x' into notifications

parents a3c3a96a e6fb0c95
...@@ -141,3 +141,5 @@ Temporary Items ...@@ -141,3 +141,5 @@ Temporary Items
.apdisk .apdisk
# End of https://www.gitignore.io/api/osx # End of https://www.gitignore.io/api/osx
app/libs/
#!/bin/bash
CURRENT_DIR=`pwd`
# The SDK dir should be 2 directories up in the tree, so we use dirname 2 times
# to get the common parent dir of the SDK and the app
tmp=`dirname $CURRENT_DIR`
tmp=`dirname $tmp`
SDK_DIR="$tmp/Rocket.Chat.Kotlin.SDK"
echo "CURRENT DIR: $CURRENT_DIR"
echo "SDK DIR: $SDK_DIR"
# check if there are changes not commited
function git_stat {
local __resultvar=$1
cd $SDK_DIR && git diff --shortstat --exit-code
eval $__resultvar="'$?'"
}
# check for changes already on the index not commited
function git_stat_cached {
local __resultvar=$1
cd $SDK_DIR && git diff --cached --shortstat --exit-code
eval $__resultvar="'$?'"
}
# get the SHA of the lastest commit
function git_sha {
temp_sha=`cd $SDK_DIR && git rev-parse --short HEAD`
echo "$temp_sha"
}
# check if the tree is dirty (has modifications not commited yet)
function check_git_dirty {
git_stat stat
git_stat_cached cached
if [ $stat -eq 0 ] && [ $cached -eq 0 ]; then
echo "not dirty"
return 1
else
echo "is dirty"
return 0
fi
}
# check if the saved last commit is the same as the latest SHA in the tree
function check_last_commit {
if [ ! -f $SDK_DIR/.last_commit_hash ]; then
echo "last_commit_hash not found"
return 0
fi
saved_hash=`cat $SDK_DIR/.last_commit_hash`
last_hash=$(git_sha)
#`cd $SDK_DIR && git rev-parse --short HEAD`
if [ "$saved_hash" == "$last_hash" ]; then
echo "same hash as before $saved_hash = $last_hash"
return 1
fi
echo "different commits, build again"
return 0
}
SHA=$(git_sha)
echo "CURRENT SHA: $SHA"
# if the tree is not dirty, there is no new commit and the .jars are still there, just skip the build
if ! check_git_dirty && ! check_last_commit && [ -f $CURRENT_DIR/libs/common-$SHA.jar ] && [ -f $CURRENT_DIR/libs/core-$SHA.jar ]; then
echo "NO BUILD NEEDED"
exit 0
fi
cd $SDK_DIR && ./gradlew common:assemble && cd $CURRENT_DIR
cd $SDK_DIR && ./gradlew core:assemble && cd $CURRENT_DIR
rm $CURRENT_DIR/libs/common* $CURRENT_DIR/libs/core*
mkdir -p $CURRENT_DIR/libs/
cp -v $SDK_DIR/common/build/libs/common-0.1-SNAPSHOT.jar $CURRENT_DIR/libs/common-$SHA.jar
cp -v $SDK_DIR/core/build/libs/core-0.1-SNAPSHOT.jar $CURRENT_DIR/libs/core-$SHA.jar
echo "$SHA" > $SDK_DIR/.last_commit_hash
exit 0
...@@ -22,6 +22,10 @@ android { ...@@ -22,6 +22,10 @@ android {
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
debug {
applicationIdSuffix ".dev"
}
} }
packagingOptions { packagingOptions {
...@@ -32,7 +36,6 @@ android { ...@@ -32,7 +36,6 @@ android {
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':core')
implementation project(':player') implementation project(':player')
implementation libraries.kotlin implementation libraries.kotlin
...@@ -76,7 +79,11 @@ dependencies { ...@@ -76,7 +79,11 @@ dependencies {
implementation libraries.androidSvg implementation libraries.androidSvg
implementation "com.google.android.gms:play-services-gcm:11.8.0" implementation libraries.playServicesGcm
implementation libraries.aVLoadingIndicatorView
implementation libraries.textDrawable
testImplementation libraries.junit testImplementation libraries.junit
androidTestImplementation (libraries.expressoCore , { androidTestImplementation (libraries.expressoCore , {
...@@ -94,4 +101,11 @@ kotlin { ...@@ -94,4 +101,11 @@ kotlin {
} }
} }
// FIXME - build and install the sdk into the app/libs directory
// We were having some issues with the kapt generated files from the sdk when importing as a module
task compileSdk(type:Exec) {
commandLine './build-sdk.sh'
}
preBuild.dependsOn compileSdk
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'
...@@ -13,7 +13,6 @@ ...@@ -13,7 +13,6 @@
<uses-permission android:name="chat.rocket.android.permission.C2D_MESSAGE" /> <uses-permission android:name="chat.rocket.android.permission.C2D_MESSAGE" />
<application <application
android:name=".app.RocketChatApplication" android:name=".app.RocketChatApplication"
android:allowBackup="true" android:allowBackup="true"
...@@ -27,7 +26,8 @@ ...@@ -27,7 +26,8 @@
android:name=".authentication.ui.AuthenticationActivity" android:name=".authentication.ui.AuthenticationActivity"
android:configChanges="orientation" android:configChanges="orientation"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/AuthenticationTheme"> android:theme="@style/AuthenticationTheme"
android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
</activity> </activity>
<activity <activity
android:name=".app.MainActivity" android:name=".chatrooms.ui.MainActivity"
android:theme="@style/ChatListTheme" /> android:theme="@style/ChatListTheme" />
<activity <activity
...@@ -72,6 +72,9 @@ ...@@ -72,6 +72,9 @@
</intent-filter> </intent-filter>
</service> </service>
<activity
android:name=".webview.WebViewActivity"
android:theme="@style/AppTheme" />
</application> </application>
</manifest> </manifest>
\ No newline at end of file
import android.content.Context import android.content.Context
import chat.rocket.android.R import chat.rocket.android.R
import org.threeten.bp.LocalDate import org.threeten.bp.*
import org.threeten.bp.LocalDateTime
import org.threeten.bp.Period
import org.threeten.bp.format.DateTimeFormatter import org.threeten.bp.format.DateTimeFormatter
import org.threeten.bp.format.FormatStyle import org.threeten.bp.format.FormatStyle
import org.threeten.bp.format.TextStyle import org.threeten.bp.format.TextStyle
...@@ -14,20 +12,20 @@ object DateTimeHelper { ...@@ -14,20 +12,20 @@ object DateTimeHelper {
private val lastWeek = today.minusWeeks(1) private val lastWeek = today.minusWeeks(1)
/** /**
* Returns a date from a LocalDateTime or the textual representation if the LocalDateTime has a max period of a week from the current date. * Returns a date from a [LocalDateTime] or the textual representation if the [LocalDateTime] has a max period of a week from the current date.
* *
* @param localDateTime The LocalDateTime. * @param localDateTime The [LocalDateTime].
* @param context The context. * @param context The context.
* @return The date or the textual representation from a LocalDateTime. * @return The date or the textual representation from a [LocalDateTime].
*/ */
fun getDate(localDateTime: LocalDateTime, context: Context): String { fun getDate(localDateTime: LocalDateTime, context: Context): String {
val localDate = localDateTime.toLocalDate() val localDate = localDateTime.toLocalDate()
return when (localDate) { return when (localDate) {
today -> localDateTime.toLocalTime().toString() today -> formatLocalTime(localDateTime.toLocalTime())
yesterday -> context.getString(R.string.msg_yesterday) yesterday -> context.getString(R.string.msg_yesterday)
else -> { else -> {
if (Period.between(lastWeek, localDate).days <= 0) { if (Period.between(lastWeek, localDate).days <= 0) {
formatDate(localDate) formatLocalDate(localDate)
} else { } else {
localDate.dayOfWeek.getDisplayName(TextStyle.FULL, Locale.getDefault()) localDate.dayOfWeek.getDisplayName(TextStyle.FULL, Locale.getDefault())
} }
...@@ -36,18 +34,33 @@ object DateTimeHelper { ...@@ -36,18 +34,33 @@ object DateTimeHelper {
} }
/** /**
* Returns a time from a LocalDateTime. * Returns a time from a [LocalDateTime].
* *
* @param localDateTime The LocalDateTime. * @param localDateTime The [LocalDateTime].
* @return The time from a LocalDateTime. * @return The time from a [LocalDateTime].
*/ */
fun getTime(localDateTime: LocalDateTime): String { fun getTime(localDateTime: LocalDateTime): String {
val formatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT) val formatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
return localDateTime.toLocalTime().format(formatter).toString() return localDateTime.toLocalTime().format(formatter).toString()
} }
private fun formatDate(localDate: LocalDate): String { /**
* Returns a [LocalDateTime] from a [Long].
*
* @param long The [Long]
* @return The [LocalDateTime] from a [Long].
*/
fun getLocalDateTime(long: Long): LocalDateTime {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(long), ZoneId.systemDefault())
}
private fun formatLocalDate(localDate: LocalDate): String {
val formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) val formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
return localDate.format(formatter).toString() return localDate.format(formatter).toString()
} }
private fun formatLocalTime(localTime: LocalTime): String {
val formatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
return localTime.format(formatter).toString()
}
} }
\ No newline at end of file
import android.content.Context import android.content.Context
import android.graphics.Typeface
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.support.v4.content.ContextCompat import android.support.v4.content.ContextCompat
import android.support.v4.graphics.drawable.DrawableCompat import android.support.v4.graphics.drawable.DrawableCompat
import android.widget.EditText import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.helper.TextHelper
import com.amulyakhare.textdrawable.TextDrawable
object DrawableHelper { object DrawableHelper {
private val AVATAR_BACKGROUND_HEXADECIMAL_COLORS = intArrayOf(
0xFFF44336.toInt(), 0xFFE91E63.toInt(), 0xFF9C27B0.toInt(), 0xFF673AB7.toInt(), 0xFF3F51B5.toInt(),
0xFF2196F3.toInt(), 0xFF03A9F4.toInt(), 0xFF00BCD4.toInt(), 0xFF009688.toInt(), 0xFF4CAF50.toInt(),
0xFF8BC34A.toInt(), 0xFFCDDC39.toInt(), 0xFFFFC107.toInt(), 0xFFFF9800.toInt(), 0xFFFF5722.toInt(),
0xFF795548.toInt(), 0xFF9E9E9E.toInt(), 0xFF607D8B.toInt())
/** /**
* Returns a Drawable from its ID. * Returns a Drawable from its ID.
* *
...@@ -37,7 +44,7 @@ object DrawableHelper { ...@@ -37,7 +44,7 @@ object DrawableHelper {
* @see wrapDrawables * @see wrapDrawables
* @see tintDrawable * @see tintDrawable
*/ */
fun wrapDrawable(drawable: Drawable) = DrawableCompat.wrap(drawable) fun wrapDrawable(drawable: Drawable): Drawable = DrawableCompat.wrap(drawable)
/** /**
* Tints an array of Drawable. * Tints an array of Drawable.
...@@ -116,4 +123,29 @@ object DrawableHelper { ...@@ -116,4 +123,29 @@ object DrawableHelper {
} }
return userStatusDrawable return userStatusDrawable
} }
/**
* Returns a drawable with the first character from a string.
*
* @param string The string to get its first character and to get the avatar background color.
* @return A drawable with the string first character.
*/
fun getTextDrawable(string: String): Drawable {
return TextDrawable.builder()
.beginConfig()
.useFont(Typeface.SANS_SERIF)
.endConfig()
.buildRound(TextHelper.getFirstCharacter(string), getAvatarBackgroundColor(string))
}
/**
* Returns a background color to be rendered on the avatar.
*
* @param string Gets the background color based on the provided string.
* @return A hexadecimal color.
* @see (Rocket.Chat/server/startup/avatar.js)
*/
private fun getAvatarBackgroundColor(string: String): Int {
return AVATAR_BACKGROUND_HEXADECIMAL_COLORS[string.length % AVATAR_BACKGROUND_HEXADECIMAL_COLORS.size]
}
} }
\ No newline at end of file
package chat.rocket.android.app
import android.app.Activity
import android.graphics.Rect
import android.util.Log
import android.view.View
import android.view.ViewTreeObserver
import android.widget.FrameLayout
//TODO: check if this code has memory leak.
class LayoutHelper {
private var childOfContent: View? = null
private var usableHeightPrevious: Int = 0
private var frameLayoutParams: FrameLayout.LayoutParams? = null
/**
* Workaround to adjust the layout when in the full screen mode.
*
* The original author of this code is Joseph Johnson and you can see his answer here: https://stackoverflow.com/a/19494006/4744263
*
* Note that this function has some differences from the original, like using *frameLayoutParams.height = usableHeightNow* instead of
* *frameLayoutParams.height = usableHeightSansKeyboard* (RobertoAllende's comment - from the same link above).
*
* @param activity The Activity to adjust the layout.
*/
fun install(activity: Activity) {
try {
val content = activity.findViewById<View>(android.R.id.content) as FrameLayout
childOfContent = content.getChildAt(0)
childOfContent?.viewTreeObserver?.addOnGlobalLayoutListener(listener)
frameLayoutParams = childOfContent?.layoutParams as FrameLayout.LayoutParams
} catch (exception : ClassCastException) {
// TODO: are we using the android.util.Log for logging that type of errors? or should we use the SDK logger?
Log.e("ERROR", exception.message)
}
}
private val listener = ViewTreeObserver.OnGlobalLayoutListener {
resizeChildOfContent()
}
private fun resizeChildOfContent() {
val usableHeightNow = computeUsableHeight()
if (usableHeightNow != usableHeightPrevious) {
val usableHeightSansKeyboard = childOfContent?.rootView?.height ?: 0
val heightDifference = usableHeightSansKeyboard - usableHeightNow
if (heightDifference > usableHeightSansKeyboard / 4) {
// keyboard probably just became visible
frameLayoutParams?.height = usableHeightSansKeyboard - heightDifference
} else {
// keyboard probably just became hidden
frameLayoutParams?.height = usableHeightNow
}
childOfContent?.requestLayout()
usableHeightPrevious = usableHeightNow
}
}
private fun computeUsableHeight(): Int {
val rect = Rect()
childOfContent?.getWindowVisibleDisplayFrame(rect)
return rect.bottom - rect.top
}
fun remove() {
childOfContent?.viewTreeObserver?.removeOnGlobalLayoutListener(listener)
childOfContent = null
frameLayoutParams = null
}
}
\ No newline at end of file
package chat.rocket.android.app
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import chat.rocket.android.R
import chat.rocket.android.app.chatlist.ChatListFragment
import chat.rocket.android.util.addFragment
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
addFragment("ChatListFragment", R.id.fragment_container) {
ChatListFragment()
}
}
}
\ No newline at end of file
package chat.rocket.android.app.chatlist
import chat.rocket.android.app.User
import org.threeten.bp.LocalDateTime
data class Chat(val user: User,
val name: String,
val type: String,
// Todo replace to Message type instead of String
val lastMessage: String,
val lastMessageDateTime: LocalDateTime,
val totalUnreadMessages: Int)
\ No newline at end of file
package chat.rocket.android.app.chatlist
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.widget.DividerItemDecoration
import android.support.v7.widget.LinearLayoutManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import chat.rocket.android.R
import chat.rocket.android.app.User
import kotlinx.android.synthetic.main.fragment_chat_list.*
import org.threeten.bp.LocalDateTime
class ChatListFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_chat_list, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
showChatList(createDumpData())
}
// This is just a sample showing 8 chat rooms (aka user subscription). We need to get it rid in a real word. REMARK: remove this comment and this method.
private fun createDumpData(): List<Chat> {
val filipe = User("1", "Filipe Brito", "filipe.brito", "online", "https://open.rocket.chat/avatar/filipe.brito")
val lucio = User("2", "Lucio Maciel", "Lucio Maciel", "busy", "https://open.rocket.chat/avatar/lucio.maciel")
val leonardo = User("3", "Leonardo Aramaki", "leonardo.aramaki", "busy", "https://open.rocket.chat/avatar/leonardo.aramaki")
val sing = User("4", "Filipe Brito", "filipe.brito", "online", "https://open.rocket.chat/avatar/filipe.brito")
val hetal = User("5", "Filipe Brito", "filipe.brito", "online", "https://open.rocket.chat/avatar/filipe.brito")
val matheus = User("6", "Filipe Brito", "filipe.brito", "online", "https://open.rocket.chat/avatar/filipe.brito")
val bestrun = User("7", "Filipe Brito", "filipe.brito", "online", "https://open.rocket.chat/avatar/filipe.brito")
val djc = User("8", "3djc", "3djc", "online", "https://open.rocket.chat/avatar/filipe.brito")
val dumpChat1 = Chat(leonardo,
"Leonardo Aramaki",
"d",
"Type something",
LocalDateTime.of(2017, 11, 21,1, 3),
1)
val dumpChat2 = Chat(filipe,
"Filipe Brito",
"d",
"Type something...Type something...Type something",
LocalDateTime.of(2017, 11, 22,1, 3),
150)
val dumpChat3 = Chat(lucio,
"Lucio Maciel",
"d",
"Type something",
LocalDateTime.of(2017, 11, 17,1, 3),
0)
val dumpChat4 = Chat(sing,
"mobile-internal",
"p",
"@aaron.ogle @rafael.kellermann same problem over here. Although all the servers show up on the selection.",
LocalDateTime.of(2017, 11, 15,1, 3),
0)
val dumpChat5 = Chat(hetal,
"general",
"c",
"Has joined the channel.",
LocalDateTime.of(2017, 11, 13,1, 3),
0)
val dumpChat6 = Chat(matheus,
"androidnativeapp",
"c",
"Yes @sttyru, but you'll need to implement from the ground up following the docs at docs.rocket.chat where you can see the REST (HTTP) and Real-Time (WebSockets) calls.",
LocalDateTime.of(2017, 11, 14,1, 3),
0)
val dumpChat7 = Chat(bestrun,
"androidnativeapp-2",
"c",
"Just downloaded .zip and imported into Android Studio then build the project.",
LocalDateTime.of(2017, 11, 4,12, 47),
0)
val dumpChat8 = Chat(djc,
"iosnativeapp",
"c",
"Ok, got confused by the blog announcement that shows a screenshot with github oAuth ! Sorry !",
LocalDateTime.of(2017, 11, 4,12, 43),
0)
// creates a list of chat sorted by lastMessageDateTime attribute.
return listOf(dumpChat1, dumpChat2, dumpChat3, dumpChat4, dumpChat5, dumpChat6, dumpChat7, dumpChat8).sortedByDescending { chat -> chat.lastMessageDateTime }
}
// REMARK: The presenter should call this method. The presenter also need to sort the chat list by latest message (compared by its date).
private fun showChatList(dataSet: List<Chat>) {
activity?.apply {
recycler_view.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
recycler_view.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
recycler_view.adapter = ChatListAdapter(dataSet.toMutableList(), this)
}
}
}
...@@ -8,7 +8,7 @@ import android.view.View ...@@ -8,7 +8,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.app.User import chat.rocket.android.app.User
import kotlinx.android.synthetic.main.fragment_chat_list.* import kotlinx.android.synthetic.main.fragment_chat_rooms.*
import org.threeten.bp.LocalDateTime import org.threeten.bp.LocalDateTime
class MessageFragment : Fragment() { class MessageFragment : Fragment() {
......
package chat.rocket.android.authentication.di package chat.rocket.android.authentication.di
import chat.rocket.android.authentication.infraestructure.AuthTokenRepository import android.content.Context
import chat.rocket.android.authentication.presentation.AuthenticationNavigator import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.authentication.ui.AuthenticationActivity import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.dagger.scope.PerActivity import chat.rocket.android.dagger.scope.PerActivity
...@@ -13,13 +13,7 @@ class AuthenticationModule { ...@@ -13,13 +13,7 @@ class AuthenticationModule {
@Provides @Provides
@PerActivity @PerActivity
fun provideAuthenticationNavigator(activity: AuthenticationActivity) = AuthenticationNavigator(activity) fun provideAuthenticationNavigator(activity: AuthenticationActivity, context: Context) = AuthenticationNavigator(activity, context)
@Provides
@PerActivity
fun provideAuthTokenRepository(): AuthTokenRepository {
return AuthTokenRepository()
}
@Provides @Provides
fun provideJob(): Job { fun provideJob(): Job {
......
...@@ -12,6 +12,7 @@ import kotlinx.coroutines.experimental.Job ...@@ -12,6 +12,7 @@ import kotlinx.coroutines.experimental.Job
@Module @Module
@PerFragment @PerFragment
class LoginFragmentModule { class LoginFragmentModule {
@Provides @Provides
fun loginView(frag: LoginFragment): LoginView { fun loginView(frag: LoginFragment): LoginView {
return frag return frag
...@@ -26,4 +27,4 @@ class LoginFragmentModule { ...@@ -26,4 +27,4 @@ class LoginFragmentModule {
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy { fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs) return CancelStrategy(owner, jobs)
} }
} }
\ No newline at end of file
package chat.rocket.android.authentication.login.presentation package chat.rocket.android.authentication.login.presentation
import chat.rocket.android.authentication.infraestructure.AuthTokenRepository
import chat.rocket.android.authentication.presentation.AuthenticationNavigator import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.SharedPreferencesRepository import chat.rocket.android.infrastructure.SharedPreferencesRepository
import chat.rocket.android.util.launchUI import chat.rocket.android.util.launchUI
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.RocketChatTwoFactorException import chat.rocket.common.RocketChatTwoFactorException
import chat.rocket.common.util.PlatformLogger
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.login import chat.rocket.core.internal.rest.login
import chat.rocket.core.internal.rest.registerPushToken import chat.rocket.core.internal.rest.registerPushToken
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import javax.inject.Inject import javax.inject.Inject
class LoginPresenter @Inject constructor(private val view: LoginView, class LoginPresenter @Inject constructor(private val view: LoginView,
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator, private val navigator: AuthenticationNavigator) {
private val okHttpClient: OkHttpClient, @Inject lateinit var client: RocketChatClient
private val logger: PlatformLogger,
private val repository: AuthTokenRepository) {
val client: RocketChatClient = RocketChatClient.create { fun authenticate(usernameOrEmail: String, password: String) {
httpClient = okHttpClient when {
restUrl = HttpUrl.parse(navigator.currentServer)!! usernameOrEmail.isBlank() -> {
websocketUrl = navigator.currentServer!! view.alertWrongUsernameOrEmail()
tokenRepository = repository
platformLogger = logger
} }
password.isEmpty() -> {
fun authenticate(username: String, password: String) { view.alertWrongPassword()
// TODO - validate input }
else -> {
launchUI(strategy) { launchUI(strategy) {
if (NetworkHelper.hasInternetAccess()) {
view.showLoading() view.showLoading()
try { try {
val token = client.login(username, password) client.login(usernameOrEmail, password) // TODO This function returns a user token so should we save it?
registerPushToken() registerPushToken()
navigator.toChatList() navigator.toChatList()
} catch (ex: RocketChatException) { } catch (exception: RocketChatException) {
when (ex) { if (exception is RocketChatTwoFactorException) {
is RocketChatTwoFactorException -> navigator.toTwoFA(usernameOrEmail, password)
navigator.toTwoFA(navigator.currentServer!!, username, password) } else {
else -> val message = exception.message
view.onLoginError(ex.message) if (message != null) {
view.showMessage(message)
} else {
view.showGenericErrorMessage()
} }
} finally { }
}
view.hideLoading() view.hideLoading()
} else {
view.showNoInternetConnection()
}
}
} }
} }
} }
fun signup() { fun signup() {
navigator.toSignUp(navigator.currentServer!!) navigator.toSignUp()
} }
private suspend fun registerPushToken() { private suspend fun registerPushToken() {
......
package chat.rocket.android.authentication.login.presentation package chat.rocket.android.authentication.login.presentation
import chat.rocket.android.core.behaviours.InternetView
import chat.rocket.android.core.behaviours.LoadingView import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
interface LoginView : LoadingView { interface LoginView : LoadingView, MessageView, InternetView {
fun onLoginError(message: String?)
/**
* Shows the oauth view if the server settings allow the login via social accounts.
*
* REMARK: we must show at maximum *three* social accounts views ([enableLoginByFacebook], [enableLoginByGithub], [enableLoginByGoogle],
* [enableLoginByLinkedin], [enableLoginByMeteor], [enableLoginByTwitter] or [enableLoginByGitlab]) for the oauth view.
* If the possibility of login via social accounts exceeds 3 different ways we should set up the FAB ([setupFabListener]) to show the remaining view(s).
*
* @param value True to show the oauth view, false otherwise.
*/
fun showOauthView(value: Boolean)
/**
* Setups the FloatingActionButton to show more social accounts views (expanding the oauth view interface to show the remaining view(s)).
*/
fun setupFabListener()
/**
* Shows the login by Facebook view.
*/
fun enableLoginByFacebook()
/**
* Shows the login by Github view.
*/
fun enableLoginByGithub()
/**
* Shows the login by Google view.
*/
fun enableLoginByGoogle()
/**
* Shows the login by Linkedin view.
*/
fun enableLoginByLinkedin()
/**
* Shows the login by Meteor view.
*/
fun enableLoginByMeteor()
/**
* Shows the login by Twitter view.
*/
fun enableLoginByTwitter()
/**
* Shows the login by Gitlab view.
*/
fun enableLoginByGitlab()
/**
* Shows the sign up view if the server settings allow the new users registration.
*
* @param value True to show the sign up view, false otherwise.
*/
fun showSignUpView(value: Boolean)
/**
* Alerts the user about a wrong inputted username or email.
*/
fun alertWrongUsernameOrEmail()
/**
* Alerts the user about a wrong inputted password.
*/
fun alertWrongPassword()
} }
\ No newline at end of file
package chat.rocket.android.authentication.login.ui package chat.rocket.android.authentication.login.ui
import DrawableHelper import DrawableHelper
import android.app.ProgressDialog import android.content.Context
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.text.style.ClickableSpan
import android.view.* import android.view.*
import android.view.inputmethod.InputMethodManager
import android.widget.ScrollView import android.widget.ScrollView
import android.widget.Toast import android.widget.Toast
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.app.KeyboardHelper
import chat.rocket.android.authentication.login.presentation.LoginPresenter import chat.rocket.android.authentication.login.presentation.LoginPresenter
import chat.rocket.android.authentication.login.presentation.LoginView import chat.rocket.android.authentication.login.presentation.LoginView
import chat.rocket.android.helper.AnimationHelper
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.TextHelper
import chat.rocket.android.util.inflate
import chat.rocket.android.util.setVisibility
import chat.rocket.android.util.textContent
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_log_in.* import kotlinx.android.synthetic.main.fragment_authentication_log_in.*
import javax.inject.Inject import javax.inject.Inject
class LoginFragment : Fragment(), LoginView { class LoginFragment : Fragment(), LoginView {
@Inject lateinit var presenter: LoginPresenter
@Inject lateinit var appContext: Context // TODO we really need it? Check alternatives...
companion object { /*
private const val SERVER_URL = "server_url" private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
if (KeyboardHelper.isSoftKeyboardShown(scroll_view.rootView)) {
fun newInstance(url: String) = LoginFragment().apply { showSignUpView(false)
arguments = Bundle(1).apply { showOauthView(false)
putString(SERVER_URL, url) showLoginButton(true)
} else {
if (isEditTextEmpty()) {
showSignUpView(true)
showOauthView(true)
showLoginButton(false)
} }
} }
} }
private var isGlobalLayoutListenerSetUp = false
var progress: ProgressDialog? = null */
lateinit var serverUrl: String companion object {
fun newInstance() = LoginFragment()
@Inject }
lateinit var presenter: LoginPresenter
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
AndroidSupportInjection.inject(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
// TODO - research a better way to initialize parameters on fragments.
serverUrl = arguments?.getString(SERVER_URL) ?: "https://open.rocket.chat"
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_authentication_log_in, container, false) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_authentication_log_in)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
activity?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN) activity?.apply {
text_username_or_email.requestFocus()
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(text_username_or_email, InputMethodManager.SHOW_IMPLICIT)
}
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
tintEditTextDrawableStart() tintEditTextDrawableStart()
} }
// Just an example: if the server allow the login via social accounts (oauth authentication) then show the respective interface. button_log_in.setOnClickListener {
shouldShowOauthView(true) presenter.authenticate(text_username_or_email.textContent, text_password.textContent)
// In this case we need to setup the layout to hide and show the oauth interface when the soft keyboard is shown (means that the user touched the text_username_or_email and text_password EditText). }
setupGlobalLayoutListener()
/*
// TODO: THIS IS A PRESENTER CONCERN - REMOVE THAT ! WE SHOULD GET THE SERVER SETTINGS!
// -------------------------------------------------------------------------------------------------------------------
showOauthView(true)
// Show the first three social account's ImageButton (REMARK: we must show at maximum *three* views) // Show the first three social account's ImageButton (REMARK: we must show at maximum *three* views)
showLoginUsingFacebookImageButton() enableLoginByFacebook()
showLoginUsingGithubImageButton() enableLoginByGithub()
showLoginUsingGoogleImageButton() enableLoginByGoogle()
// Setup the FloatingActionButton to show more social account's ImageButton (it expands the social accounts interface to show more views).
setupFabListener() setupFabListener()
// Just an example: if the server allow the new users registration then show the respective interface. // Just an example: if the server allow the new users registration then show the respective interface.
shouldShowSignUpMsgView(true) setupSignUpListener()
showSignUpView(true)
button_log_in.setOnClickListener { // -------------------------------------------------------------------------------------------------------------------
presenter.authenticate(text_username_or_email.text.toString(), text_password.text.toString()) */
}
text_new_to_rocket_chat.setOnClickListener {
presenter.signup()
} }
} /*
override fun onDestroyView() { override fun onDestroyView() {
scroll_view.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
super.onDestroyView() super.onDestroyView()
if (isGlobalLayoutListenerSetUp) {
scroll_view.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
isGlobalLayoutListenerSetUp = false
} }
private fun tintEditTextDrawableStart() {
activity?.applicationContext?.apply {
val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_assignment_ind_black_24dp, this)
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_24dp, this)
val drawables = arrayOf(personDrawable, lockDrawable)
DrawableHelper.wrapDrawables(drawables)
DrawableHelper.tintDrawables(drawables, this, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawables(arrayOf(text_username_or_email, text_password), drawables)
} }
*/
override fun showOauthView(value: Boolean) {
// if (value) {
// social_accounts_container.setVisibility(true)
// button_fab.setVisibility(true)
//
// // We need to setup the layout to hide and show the oauth interface when the soft keyboard is shown
// // (means that the user touched the text_username_or_email or text_password EditText to fill that respective fields).
// if (!isGlobalLayoutListenerSetUp) {
// scroll_view.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
// isGlobalLayoutListenerSetUp = true
// }
// } else {
// social_accounts_container.setVisibility(false)
// button_fab.setVisibility(false)
// }
} }
private fun setupGlobalLayoutListener() { override fun setupFabListener() {
scroll_view.viewTreeObserver.addOnGlobalLayoutListener(layoutListener) // button_fab.setOnClickListener({
// button_fab.hide()
// showRemainingSocialAccountsView()
// scrollToBottom()
// })
} }
val layoutListener = ViewTreeObserver.OnGlobalLayoutListener { override fun enableLoginByFacebook() {
if (KeyboardHelper.isSoftKeyboardShown(scroll_view.rootView)) { button_facebook.setVisibility(true)
shouldShowOauthView(false)
shouldShowSignUpMsgView(false)
shouldShowLoginButton(true)
} else {
if (isEditTextNullOrBlank()) {
shouldShowOauthView(true)
shouldShowSignUpMsgView(true)
shouldShowLoginButton(false)
}
}
} }
private fun shouldShowOauthView(show: Boolean) { override fun enableLoginByGithub() {
if (show) { button_github.setVisibility(true)
social_accounts_container.visibility = View.VISIBLE
button_fab.visibility = View.VISIBLE
} else {
social_accounts_container.visibility = View.GONE
button_fab.visibility = View.GONE
}
} }
private fun shouldShowSignUpMsgView(show: Boolean) { override fun enableLoginByGoogle() {
if (show) { button_google.setVisibility(true)
text_new_to_rocket_chat.visibility = View.VISIBLE
} else {
text_new_to_rocket_chat.visibility = View.GONE
}
} }
private fun shouldShowLoginButton(show: Boolean) { override fun enableLoginByLinkedin() {
if (show) { button_linkedin.setVisibility(true)
button_log_in.visibility = View.VISIBLE
} else {
button_log_in.visibility = View.GONE
}
} }
private fun showLoginUsingFacebookImageButton() { override fun enableLoginByMeteor() {
button_facebook.visibility = View.VISIBLE button_meteor.setVisibility(true)
} }
private fun showLoginUsingGithubImageButton() { override fun enableLoginByTwitter() {
button_github.visibility = View.VISIBLE button_twitter.setVisibility(true)
} }
private fun showLoginUsingGoogleImageButton() { override fun enableLoginByGitlab() {
button_google.visibility = View.VISIBLE button_gitlab.setVisibility(true)
} }
private fun showLoginUsingLinkedinImageButton() { override fun showSignUpView(value: Boolean) = text_new_to_rocket_chat.setVisibility(value)
button_linkedin.visibility = View.VISIBLE
override fun alertWrongUsernameOrEmail() {
AnimationHelper.vibrateSmartPhone(appContext)
AnimationHelper.shakeView(text_username_or_email)
text_username_or_email.requestFocus()
} }
private fun showLoginUsingMeteorImageButton() { override fun alertWrongPassword() {
button_meteor.visibility = View.VISIBLE AnimationHelper.vibrateSmartPhone(appContext)
AnimationHelper.shakeView(text_password)
text_password.requestFocus()
} }
private fun showLoginUsingTwitterImageButton() { override fun showLoading() {
button_twitter.visibility = View.VISIBLE enableUserInput(false)
view_loading.setVisibility(true)
} }
private fun showLoginUsingGitlabImageButton() { override fun hideLoading() {
button_gitlab.visibility = View.VISIBLE view_loading.setVisibility(false)
enableUserInput(true)
} }
private fun setupFabListener() { override fun showMessage(message: String) = Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
button_fab.setOnClickListener({
showLoginUsingLinkedinImageButton()
showLoginUsingMeteorImageButton()
showLoginUsingTwitterImageButton()
showLoginUsingGitlabImageButton()
scrollToBottom()
hideFab()
})
}
// Returns true if *all* EditText are null or blank. override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
private fun isEditTextNullOrBlank(): Boolean {
return text_username_or_email.text.isNullOrBlank() && text_password.text.isNullOrBlank()
}
private fun scrollToBottom() {
scroll_view.postDelayed({
scroll_view.fullScroll(ScrollView.FOCUS_DOWN)
}, 1000)
}
private fun hideFab() { override fun showNoInternetConnection() = showMessage(getString(R.string.msg_no_internet_connection))
button_fab.postDelayed({
button_fab.hide()
}, 1500) private fun tintEditTextDrawableStart() {
activity?.apply {
val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_assignment_ind_black_24dp, this)
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_24dp, this)
val drawables = arrayOf(personDrawable, lockDrawable)
DrawableHelper.wrapDrawables(drawables)
DrawableHelper.tintDrawables(drawables, this, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawables(arrayOf(text_username_or_email, text_password), drawables)
}
}
/*
private fun showLoginButton(value: Boolean) {
button_log_in.setVisibility(value)
} }
override fun showLoading() { private fun setupSignUpListener() {
// TODO - change for a proper progress indicator val signUp = getString(R.string.title_sign_up)
progress = ProgressDialog.show(activity, "Authenticating", val newToRocketChat = String.format(getString(R.string.msg_new_to_rocket_chat), signUp)
"Verifying user credentials", true, true)
text_new_to_rocket_chat.text = newToRocketChat
val signUpListener = object : ClickableSpan() {
override fun onClick(view: View) = presenter.signup()
} }
override fun hideLoading() { TextHelper.addLink(text_new_to_rocket_chat, arrayOf(signUp), arrayOf(signUpListener))
progress?.apply {
cancel()
} }
progress = null */
private fun enableUserInput(value: Boolean) {
button_log_in.isEnabled = value
text_username_or_email.isEnabled = value
text_password.isEnabled = value
// if (isEditTextEmpty()) {
// showSignUpView(value)
// showOauthView(value)
// }
} }
/*
// Returns true if *all* EditTexts are empty.
private fun isEditTextEmpty(): Boolean = text_username_or_email.textContent.isBlank() && text_password.textContent.isEmpty()
override fun onLoginError(message: String?) { private fun showRemainingSocialAccountsView() {
// TODO - show a proper error message social_accounts_container.postDelayed({
Toast.makeText(activity, message, Toast.LENGTH_LONG).show() enableLoginByLinkedin()
enableLoginByMeteor()
enableLoginByTwitter()
enableLoginByGitlab()
}, 1000)
}
private fun scrollToBottom() {
scroll_view.postDelayed({
scroll_view.fullScroll(ScrollView.FOCUS_DOWN)
}, 1250)
} }
*/
} }
\ No newline at end of file
package chat.rocket.android.authentication.presentation package chat.rocket.android.authentication.presentation
import android.content.Context
import android.content.Intent import android.content.Intent
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.app.MainActivity import chat.rocket.android.chatrooms.ui.MainActivity
import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.authentication.login.ui.LoginFragment import chat.rocket.android.authentication.login.ui.LoginFragment
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.util.addFragmentBackStack import chat.rocket.android.util.addFragmentBackStack
import chat.rocket.android.webview.webViewIntent
class AuthenticationNavigator(internal val activity: AuthenticationActivity) { class AuthenticationNavigator(internal val activity: AuthenticationActivity, internal val context: Context) {
var currentServer: String? = null lateinit var server: String
lateinit var usernameOrEmail: String
lateinit var password: String
fun toLogin(server: String) { fun toLogin(server: String) {
currentServer = server this.server = server
activity.addFragmentBackStack("loginFragment", R.id.fragment_container) { activity.addFragmentBackStack("loginFragment", R.id.fragment_container) {
LoginFragment.newInstance(server) LoginFragment.newInstance()
} }
} }
fun toChatList() { fun toTwoFA(usernameOrEmail: String, password: String) {
val chatRoom = Intent(activity, MainActivity::class.java).apply { this.usernameOrEmail = usernameOrEmail
//TODO any parameter to pass this.password = password
activity.addFragmentBackStack("twoFAFragment", R.id.fragment_container) {
TwoFAFragment.newInstance()
} }
activity.startActivity(chatRoom)
activity.finish()
} }
fun toTwoFA(server: String, username: String, password: String) { fun toSignUp() {
currentServer = server activity.addFragmentBackStack("signupFragment", R.id.fragment_container) {
activity.addFragmentBackStack("twoFAFragment", R.id.fragment_container) { SignupFragment.newInstance()
TwoFAFragment.newInstance(server, username, password)
} }
} }
fun toSignUp(server: String) { fun toTermsOfService() {
currentServer = server val webPageUrl = server + "/terms-of-service" // TODO Move to UrlHelper
activity.addFragmentBackStack("signupFragment", R.id.fragment_container) { activity.startActivity(context.webViewIntent(webPageUrl))
SignupFragment.newInstance(server) activity.overridePendingTransition(R.anim.slide_up, R.anim.hold)
}
fun toPrivacyPolicy() {
val webPageUrl = server + "/privacy-policy" // TODO Move to UrlHelper
activity.startActivity(context.webViewIntent(webPageUrl))
activity.overridePendingTransition(R.anim.slide_up, R.anim.hold)
}
fun toChatList() {
val chatList = Intent(activity, MainActivity::class.java).apply {
//TODO any parameter to pass
} }
activity.startActivity(chatList)
activity.finish()
} }
} }
package chat.rocket.android.authentication.server.di package chat.rocket.android.authentication.server.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.authentication.server.presentation.ServerView import chat.rocket.android.authentication.server.presentation.ServerView
import chat.rocket.android.authentication.server.ui.ServerFragment import chat.rocket.android.authentication.server.ui.ServerFragment
import chat.rocket.android.core.lifecycle.CancelStrategy
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module @Module
class ServerFragmentModule { class ServerFragmentModule {
@Provides @Provides
fun serverView(frag: ServerFragment): ServerView { fun serverView(frag: ServerFragment): ServerView {
return frag return frag
} }
@Provides
fun provideLifecycleOwner(frag: ServerFragment): 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.server.presentation package chat.rocket.android.authentication.server.presentation
import chat.rocket.android.authentication.presentation.AuthenticationNavigator import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.util.launchUI
import chat.rocket.core.RocketChatClient
import javax.inject.Inject import javax.inject.Inject
class ServerPresenter @Inject constructor(private val view: ServerView, class ServerPresenter @Inject constructor(private val view: ServerView,
private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator) { private val navigator: AuthenticationNavigator) {
@Inject lateinit var client: RocketChatClient
fun connect(server: String) {
launchUI(strategy) {
if (NetworkHelper.hasInternetAccess()) {
view.showLoading()
fun login(server: String) {
// TODO - validate server URL and get server settings and info before going to Login screen // TODO - validate server URL and get server settings and info before going to Login screen
//client.connect(server)
navigator.toLogin(server) navigator.toLogin(server)
view.hideLoading()
} else {
view.showNoInternetConnection()
}
}
} }
} }
\ No newline at end of file
package chat.rocket.android.authentication.server.presentation package chat.rocket.android.authentication.server.presentation
interface ServerView import chat.rocket.android.core.behaviours.InternetView
\ No newline at end of file import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
interface ServerView : LoadingView, MessageView, InternetView
\ No newline at end of file
...@@ -5,44 +5,70 @@ import android.support.v4.app.Fragment ...@@ -5,44 +5,70 @@ import android.support.v4.app.Fragment
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowManager import android.view.ViewTreeObserver
import android.widget.Toast
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.authentication.server.presentation.ServerPresenter import chat.rocket.android.authentication.server.presentation.ServerPresenter
import chat.rocket.android.authentication.server.presentation.ServerView import chat.rocket.android.authentication.server.presentation.ServerView
import chat.rocket.android.util.ifEmpty import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.util.*
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_server.* import kotlinx.android.synthetic.main.fragment_authentication_server.*
import javax.inject.Inject import javax.inject.Inject
class ServerFragment : Fragment(), ServerView { class ServerFragment : Fragment(), ServerView {
@Inject lateinit var presenter: ServerPresenter
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
text_server_url.isCursorVisible = KeyboardHelper.isSoftKeyboardShown(relative_layout.rootView)
}
@Inject companion object {
lateinit var presenter: ServerPresenter fun newInstance() = ServerFragment()
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
AndroidSupportInjection.inject(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
} }
override fun onCreateView(inflater: LayoutInflater, override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_authentication_server)
container: ViewGroup?,
savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.fragment_authentication_server, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
relative_layout.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
setupOnClickListener()
}
text_server_url.setSelection(text_server_url.length()) override fun onDestroyView() {
super.onDestroyView()
relative_layout.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
}
activity?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) override fun showLoading() {
enableUserInput(false)
view_loading.setVisibility(true)
}
button_connect.setOnClickListener { override fun hideLoading() {
val url = text_server_url.text.toString().ifEmpty(text_server_url.hint.toString()) view_loading.setVisibility(false)
presenter.login(server_protocol_label.text.toString() + url) enableUserInput(true)
} }
override fun showMessage(message: String) = Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showNoInternetConnection() = showMessage(getString(R.string.msg_no_internet_connection))
private fun enableUserInput(value: Boolean) {
button_connect.isEnabled = value
text_server_url.isEnabled = value
} }
companion object { private fun setupOnClickListener() {
fun newInstance() = ServerFragment() button_connect.setOnClickListener {
val url = text_server_url.textContent.ifEmpty(text_server_url.hintContent)
presenter.connect(text_server_protocol.textContent + url)
}
} }
} }
\ No newline at end of file
...@@ -12,6 +12,7 @@ import kotlinx.coroutines.experimental.Job ...@@ -12,6 +12,7 @@ import kotlinx.coroutines.experimental.Job
@Module @Module
@PerFragment @PerFragment
class SignupFragmentModule { class SignupFragmentModule {
@Provides @Provides
fun signupView(frag: SignupFragment): SignupView { fun signupView(frag: SignupFragment): SignupView {
return frag return frag
......
...@@ -4,8 +4,9 @@ import chat.rocket.android.authentication.signup.ui.SignupFragment ...@@ -4,8 +4,9 @@ import chat.rocket.android.authentication.signup.ui.SignupFragment
import dagger.Module import dagger.Module
import dagger.android.ContributesAndroidInjector import dagger.android.ContributesAndroidInjector
@Module abstract class SignupFragmentProvider { @Module
abstract class SignupFragmentProvider {
@ContributesAndroidInjector(modules = arrayOf(SignupFragmentModule::class)) @ContributesAndroidInjector(modules = [SignupFragmentModule::class])
abstract fun provideSignupFragment(): SignupFragment abstract fun provideSignupFragment(): SignupFragment
} }
\ No newline at end of file
package chat.rocket.android.authentication.signup.presentation package chat.rocket.android.authentication.signup.presentation
import chat.rocket.android.authentication.infraestructure.AuthTokenRepository
import chat.rocket.android.authentication.presentation.AuthenticationNavigator import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.util.launchUI import chat.rocket.android.util.launchUI
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.util.PlatformLogger
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.login import chat.rocket.core.internal.rest.login
import chat.rocket.core.internal.rest.signup import chat.rocket.core.internal.rest.signup
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class SignupPresenter @Inject constructor(private val view: SignupView, class SignupPresenter @Inject constructor(private val view: SignupView,
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator, private val navigator: AuthenticationNavigator) {
private val okHttpClient: OkHttpClient, @Inject lateinit var client: RocketChatClient
private val logger: PlatformLogger,
private val repository: AuthTokenRepository) {
val client: RocketChatClient = RocketChatClient.create { fun signup(name: String, username: String, password: String, email: String) {
httpClient = okHttpClient when {
restUrl = HttpUrl.parse(navigator.currentServer)!! name.isBlank() -> {
websocketUrl = navigator.currentServer!! view.alertBlankName()
tokenRepository = repository
platformLogger = logger
} }
username.isBlank() -> {
fun signup(email: String, name: String, username: String, password: String) { view.alertBlankUsername()
// TODO - validate input }
password.isEmpty() -> {
view.alertEmptyPassword()
}
email.isBlank() -> {
view.alertBlankEmail()
}
else -> {
launchUI(strategy) { launchUI(strategy) {
if (NetworkHelper.hasInternetAccess()) {
view.showLoading() view.showLoading()
try { try {
val user = client.signup(email, name, username, password) client.signup(email, name, username, password) // TODO This function returns a user so should we save it?
Timber.d("Created user: $user") client.login(username, password) // TODO This function returns a user token so should we save it?
val token = client.login(username, password)
Timber.d("Logged in: $token")
navigator.toChatList() navigator.toChatList()
} catch (ex: RocketChatException) { } catch (exception: RocketChatException) {
view.onSignupError(ex.message) val errorMessage = exception.message
} finally { if (errorMessage != null) {
view.showMessage(errorMessage)
} else {
view.showGenericErrorMessage()
}
}
view.hideLoading() view.hideLoading()
} else {
view.showNoInternetConnection()
}
}
} }
} }
} }
fun termsOfService() {
navigator.toTermsOfService()
}
fun privacyPolicy() {
navigator.toPrivacyPolicy()
}
} }
\ No newline at end of file
package chat.rocket.android.authentication.signup.presentation package chat.rocket.android.authentication.signup.presentation
import chat.rocket.android.core.behaviours.InternetView
import chat.rocket.android.core.behaviours.LoadingView import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
interface SignupView : LoadingView { interface SignupView : LoadingView, MessageView, InternetView {
fun onSignupError(message: String? = "Unknown error")
/**
* Alerts the user about a blank name.
*/
fun alertBlankName()
/**
* Alerts the user about a blank username.
*/
fun alertBlankUsername()
/**
* Alerts the user about a empty password.
*/
fun alertEmptyPassword()
/**
* Alerts the user about a blank email.
*/
fun alertBlankEmail()
} }
\ No newline at end of file
package chat.rocket.android.authentication.signup.ui package chat.rocket.android.authentication.signup.ui
import DrawableHelper import DrawableHelper
import android.app.ProgressDialog import android.content.Context
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.text.style.ClickableSpan
import android.view.* import android.view.*
import android.widget.Toast import android.widget.Toast
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.app.KeyboardHelper
import chat.rocket.android.authentication.signup.presentation.SignupPresenter import chat.rocket.android.authentication.signup.presentation.SignupPresenter
import chat.rocket.android.authentication.signup.presentation.SignupView import chat.rocket.android.authentication.signup.presentation.SignupView
import chat.rocket.android.util.content import chat.rocket.android.helper.AnimationHelper
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.TextHelper
import chat.rocket.android.util.setVisibility
import chat.rocket.android.util.textContent
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_sign_up.* import kotlinx.android.synthetic.main.fragment_authentication_sign_up.*
import javax.inject.Inject import javax.inject.Inject
class SignupFragment : Fragment(), SignupView { class SignupFragment : Fragment(), SignupView {
@Inject lateinit var presenter: SignupPresenter
@Inject lateinit var appContext: Context
companion object { private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
private const val SERVER_URL = "server_url" if (KeyboardHelper.isSoftKeyboardShown(constraint_layout.rootView)) {
text_new_user_agreement.setVisibility(false)
fun newInstance(url: String) = SignupFragment().apply { } else {
arguments = Bundle(1).apply { text_new_user_agreement.setVisibility(true)
putString(SERVER_URL, url)
}
} }
} }
@Inject companion object {
lateinit var presenter: SignupPresenter fun newInstance() = SignupFragment()
var progress: ProgressDialog? = null }
lateinit var serverUrl: String
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
AndroidSupportInjection.inject(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
// TODO - research a better way to initialize parameters on fragments.
serverUrl = arguments?.getString(SERVER_URL) ?: "https://open.rocket.chat"
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_authentication_sign_up, container, false) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_authentication_sign_up, container, false)
...@@ -52,65 +52,105 @@ class SignupFragment : Fragment(), SignupView { ...@@ -52,65 +52,105 @@ class SignupFragment : Fragment(), SignupView {
tintEditTextDrawableStart() tintEditTextDrawableStart()
} }
setupGlobalLayoutListener() constraint_layout.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
button_sign_up.setOnClickListener { setUpNewUserAgreementListener()
val email = text_email.content
val name = text_name.content
val username = text_username.content
val password = text_password.content
presenter.signup(email, name, username, password) button_sign_up.setOnClickListener {
presenter.signup(text_name.textContent, text_username.textContent, text_password.textContent, text_email.textContent)
} }
} }
override fun onDestroyView() { override fun onDestroyView() {
constraint_layout.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
super.onDestroyView() super.onDestroyView()
constraint_layout.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
} }
private fun tintEditTextDrawableStart() { override fun alertBlankName() {
activity?.applicationContext?.apply { AnimationHelper.vibrateSmartPhone(appContext)
val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_person_black_24dp, this) AnimationHelper.shakeView(text_name)
val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, this) text_name.requestFocus()
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_24dp, this)
val emailDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_email_black_24dp, this)
val drawables = arrayOf(personDrawable, atDrawable, lockDrawable, emailDrawable)
DrawableHelper.wrapDrawables(drawables)
DrawableHelper.tintDrawables(drawables, this, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawables(arrayOf(text_name, text_username, text_password, text_email), drawables)
}
} }
private fun setupGlobalLayoutListener() { override fun alertBlankUsername() {
constraint_layout.viewTreeObserver.addOnGlobalLayoutListener(layoutListener) AnimationHelper.vibrateSmartPhone(appContext)
AnimationHelper.shakeView(text_username)
text_username.requestFocus()
} }
val layoutListener = ViewTreeObserver.OnGlobalLayoutListener { override fun alertEmptyPassword() {
if (KeyboardHelper.isSoftKeyboardShown(constraint_layout.rootView)) { AnimationHelper.vibrateSmartPhone(appContext)
text_new_user_agreement.visibility = View.GONE AnimationHelper.shakeView(text_password)
} else { text_password.requestFocus()
text_new_user_agreement.visibility = View.VISIBLE
} }
override fun alertBlankEmail() {
AnimationHelper.vibrateSmartPhone(appContext)
AnimationHelper.shakeView(text_email)
text_email.requestFocus()
} }
override fun showLoading() { override fun showLoading() {
// TODO - change for a proper progress indicator enableUserInput(false)
progress = ProgressDialog.show(activity, "Authenticating", view_loading.setVisibility(true)
"Registering user", true, true)
} }
override fun hideLoading() { override fun hideLoading() {
progress?.apply { view_loading.setVisibility(false)
cancel() enableUserInput(true)
}
override fun showMessage(message: String) {
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
}
override fun showGenericErrorMessage() {
showMessage(getString(R.string.msg_generic_error))
} }
progress = null
override fun showNoInternetConnection() {
Toast.makeText(activity, getString(R.string.msg_no_internet_connection), Toast.LENGTH_SHORT).show()
} }
override fun onSignupError(message: String?) { private fun tintEditTextDrawableStart() {
// TODO - show a proper error message val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_person_black_24dp, appContext)
Toast.makeText(activity, message, Toast.LENGTH_LONG).show() val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, appContext)
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_24dp, appContext)
val emailDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_email_black_24dp, appContext)
val drawables = arrayOf(personDrawable, atDrawable, lockDrawable, emailDrawable)
DrawableHelper.wrapDrawables(drawables)
DrawableHelper.tintDrawables(drawables, appContext, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawables(arrayOf(text_name, text_username, text_password, text_email), drawables)
} }
private fun setUpNewUserAgreementListener() {
val termsOfService = getString(R.string.action_terms_of_service)
val privacyPolicy = getString(R.string.action_privacy_policy)
val newUserAgreement = String.format(getString(R.string.msg_new_user_agreement), termsOfService, privacyPolicy)
text_new_user_agreement.text = newUserAgreement
val termsOfServiceListener = object : ClickableSpan() {
override fun onClick(view: View) {
presenter.termsOfService()
}
}
val privacyPolicyListener = object : ClickableSpan() {
override fun onClick(view: View) {
presenter.privacyPolicy()
}
}
TextHelper.addLink(text_new_user_agreement, arrayOf(termsOfService, privacyPolicy), arrayOf(termsOfServiceListener, privacyPolicyListener))
}
private fun enableUserInput(value: Boolean) {
button_sign_up.isEnabled = value
text_name.isEnabled = value
text_username.isEnabled = value
text_password.isEnabled = value
text_email.isEnabled = value
}
} }
...@@ -12,6 +12,7 @@ import kotlinx.coroutines.experimental.Job ...@@ -12,6 +12,7 @@ import kotlinx.coroutines.experimental.Job
@Module @Module
@PerFragment @PerFragment
class TwoFAFragmentModule { class TwoFAFragmentModule {
@Provides @Provides
fun loginView(frag: TwoFAFragment): TwoFAView { fun loginView(frag: TwoFAFragment): TwoFAView {
return frag return frag
......
...@@ -6,6 +6,6 @@ import dagger.android.ContributesAndroidInjector ...@@ -6,6 +6,6 @@ import dagger.android.ContributesAndroidInjector
@Module abstract class TwoFAFragmentProvider { @Module abstract class TwoFAFragmentProvider {
@ContributesAndroidInjector(modules = arrayOf(TwoFAFragmentModule::class)) @ContributesAndroidInjector(modules = [TwoFAFragmentModule::class])
abstract fun provideTwoFAFragment(): TwoFAFragment abstract fun provideTwoFAFragment(): TwoFAFragment
} }
package chat.rocket.android.authentication.twofactor.presentation package chat.rocket.android.authentication.twofactor.presentation
import chat.rocket.android.authentication.infraestructure.AuthTokenRepository
import chat.rocket.android.authentication.presentation.AuthenticationNavigator import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.SharedPreferencesRepository import chat.rocket.android.infrastructure.SharedPreferencesRepository
import chat.rocket.android.util.launchUI import chat.rocket.android.util.launchUI
import chat.rocket.common.RocketChatAuthException
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.util.PlatformLogger
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.login import chat.rocket.core.internal.rest.login
import chat.rocket.core.internal.rest.registerPushToken import chat.rocket.core.internal.rest.registerPushToken
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import javax.inject.Inject import javax.inject.Inject
class TwoFAPresenter @Inject constructor(private val view: TwoFAView, class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator, private val navigator: AuthenticationNavigator) {
private val okHttpClient: OkHttpClient, @Inject lateinit var client: RocketChatClient
private val logger: PlatformLogger,
private val repository: AuthTokenRepository) {
val client: RocketChatClient = RocketChatClient.create {
httpClient = okHttpClient
restUrl = HttpUrl.parse(navigator.currentServer)!!
websocketUrl = navigator.currentServer!!
tokenRepository = repository
platformLogger = logger
}
fun authenticate(username: String, password: String, pin: String) {
// TODO - validate input
fun authenticate(twoFactorAuthenticationCode: String) {
if (twoFactorAuthenticationCode.isBlank()) {
view.alertBlankTwoFactorAuthenticationCode()
} else {
launchUI(strategy) { launchUI(strategy) {
if (NetworkHelper.hasInternetAccess()) {
view.showLoading() view.showLoading()
try { try {
val token = client.login(username, password, pin) client.login(navigator.usernameOrEmail, navigator.password, twoFactorAuthenticationCode) // TODO This function returns a user token so should we save it?
registerPushToken() registerPushToken()
navigator.toChatList() navigator.toChatList()
} catch (ex: RocketChatException) { } catch (exception: RocketChatException) {
view.onLoginError(ex.message) if (exception is RocketChatAuthException) {
} finally { view.alertInvalidTwoFactorAuthenticationCode()
} else {
val message = exception.message
if (message != null) {
view.showMessage(message)
} else {
view.showGenericErrorMessage()
}
}
}
view.hideLoading() view.hideLoading()
} else {
view.showNoInternetConnection()
}
} }
} }
} }
fun signup() { fun signup() {
navigator.toSignUp(navigator.currentServer!!) navigator.toSignUp()
} }
private suspend fun registerPushToken() { private suspend fun registerPushToken() {
......
package chat.rocket.android.authentication.twofactor.presentation package chat.rocket.android.authentication.twofactor.presentation
import chat.rocket.android.authentication.login.presentation.LoginView import chat.rocket.android.core.behaviours.InternetView
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
interface TwoFAView : LoginView interface TwoFAView : LoadingView, MessageView, InternetView {
\ No newline at end of file
/**
* Alerts the user about a blank Two Factor Authentication code.
*/
fun alertBlankTwoFactorAuthenticationCode()
/**
* Alerts the user about an invalid inputted Two Factor Authentication code.
*/
fun alertInvalidTwoFactorAuthenticationCode()
}
\ No newline at end of file
package chat.rocket.android.authentication.twofactor.ui package chat.rocket.android.authentication.twofactor.ui
import DrawableHelper import DrawableHelper
import android.app.ProgressDialog import android.content.Context
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.authentication.twofactor.presentation.TwoFAPresenter import chat.rocket.android.authentication.twofactor.presentation.TwoFAPresenter
import chat.rocket.android.authentication.twofactor.presentation.TwoFAView import chat.rocket.android.authentication.twofactor.presentation.TwoFAView
import chat.rocket.android.util.content import chat.rocket.android.helper.AnimationHelper
import chat.rocket.android.util.setVisibility
import chat.rocket.android.util.textContent
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_two_fa.* import kotlinx.android.synthetic.main.fragment_authentication_two_fa.*
import javax.inject.Inject import javax.inject.Inject
class TwoFAFragment : Fragment(), TwoFAView { class TwoFAFragment : Fragment(), TwoFAView {
@Inject lateinit var presenter: TwoFAPresenter
@Inject lateinit var appContext: Context // TODO we really need it? Check alternatives...
companion object { companion object {
private const val SERVER_URL = "server_url" fun newInstance() = TwoFAFragment()
private const val USERNAME = "username"
private const val PASSWORD = "password"
fun newInstance(url: String, username: String, password: String) = TwoFAFragment().apply {
arguments = Bundle(1).apply {
putString(SERVER_URL, url)
putString(USERNAME, username)
putString(PASSWORD, password)
} }
}
}
var progress: ProgressDialog? = null
lateinit var serverUrl: String
lateinit var username: String
lateinit var password: String
@Inject
lateinit var presenter: TwoFAPresenter
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
AndroidSupportInjection.inject(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
// TODO - research a better way to initialize parameters on fragments.
serverUrl = arguments?.getString(SERVER_URL) ?: "https://open.rocket.chat"
username = arguments?.getString(USERNAME) ?: ""
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? = inflater.inflate(R.layout.fragment_authentication_two_fa, container, false)
...@@ -57,42 +38,58 @@ class TwoFAFragment : Fragment(), TwoFAView { ...@@ -57,42 +38,58 @@ class TwoFAFragment : Fragment(), TwoFAView {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
activity?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) activity?.apply {
text_two_factor_auth.requestFocus()
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(text_two_factor_auth, InputMethodManager.SHOW_IMPLICIT)
}
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
tintEditTextDrawableStart() tintEditTextDrawableStart()
} }
setupOnClickListener()
}
button_log_in.setOnClickListener { override fun alertBlankTwoFactorAuthenticationCode() {
presenter.authenticate(username, password, text_two_factor_auth.content) AnimationHelper.vibrateSmartPhone(appContext)
AnimationHelper.shakeView(text_two_factor_auth)
} }
override fun alertInvalidTwoFactorAuthenticationCode() = showMessage(getString(R.string.msg_invalid_2fa_code))
override fun showLoading() {
enableUserInput(false)
view_loading.setVisibility(true)
} }
override fun hideLoading() {
view_loading.setVisibility(false)
enableUserInput(true)
}
override fun showMessage(message: String) = Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showNoInternetConnection() = showMessage(getString(R.string.msg_no_internet_connection))
private fun tintEditTextDrawableStart() { private fun tintEditTextDrawableStart() {
activity?.applicationContext?.apply { activity?.apply {
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_vpn_key_black_24dp, this) val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_vpn_key_black_24dp, this)
DrawableHelper.wrapDrawable(lockDrawable) DrawableHelper.wrapDrawable(lockDrawable)
DrawableHelper.tintDrawable(lockDrawable, this, R.color.colorDrawableTintGrey) DrawableHelper.tintDrawable(lockDrawable, this, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawable(text_two_factor_auth, lockDrawable) DrawableHelper.compoundDrawable(text_two_factor_auth, lockDrawable)
} }
} }
override fun showLoading() { private fun enableUserInput(value: Boolean) {
// TODO - change for a proper progress indicator button_log_in.isEnabled = value
progress = ProgressDialog.show(activity, "Authenticating", text_two_factor_auth.isEnabled = value
"Verifying user credentials", true, true)
} }
override fun hideLoading() { private fun setupOnClickListener() {
progress?.apply { button_log_in.setOnClickListener {
cancel() presenter.authenticate(text_two_factor_auth.textContent)
}
progress = null
} }
override fun onLoginError(message: String?) {
// TODO - show a proper error message
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
} }
} }
\ No newline at end of file
...@@ -4,7 +4,6 @@ import android.os.Bundle ...@@ -4,7 +4,6 @@ import android.os.Bundle
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.app.LayoutHelper
import chat.rocket.android.authentication.server.ui.ServerFragment import chat.rocket.android.authentication.server.ui.ServerFragment
import chat.rocket.android.util.addFragment import chat.rocket.android.util.addFragment
import dagger.android.AndroidInjection import dagger.android.AndroidInjection
...@@ -14,29 +13,19 @@ import dagger.android.support.HasSupportFragmentInjector ...@@ -14,29 +13,19 @@ import dagger.android.support.HasSupportFragmentInjector
import javax.inject.Inject import javax.inject.Inject
class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector { class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
@Inject
lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
val layoutHelper = LayoutHelper()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
AndroidInjection.inject(this)
setContentView(R.layout.activity_authentication) setContentView(R.layout.activity_authentication)
layoutHelper.install(this) AndroidInjection.inject(this)
addFragment("authenticationServerFragment", R.id.fragment_container) { addFragment("authenticationServerFragment", R.id.fragment_container) {
ServerFragment.newInstance() ServerFragment.newInstance()
} }
} }
override fun onDestroy() {
layoutHelper.remove()
super.onDestroy()
}
override fun supportFragmentInjector(): AndroidInjector<Fragment> { override fun supportFragmentInjector(): AndroidInjector<Fragment> {
return fragmentDispatchingAndroidInjector return fragmentDispatchingAndroidInjector
} }
......
package chat.rocket.android.chatrooms.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.chatrooms.presentation.ChatRoomsView
import chat.rocket.android.chatrooms.ui.ChatRoomsFragment
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 ChatRoomsFragmentModule {
@Provides
fun chatRoomsView(frag: ChatRoomsFragment): ChatRoomsView {
return frag
}
@Provides
fun provideLifecycleOwner(frag: ChatRoomsFragment): LifecycleOwner {
return frag
}
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
@Provides
fun provideJob(): Job {
return Job()
}
}
\ No newline at end of file
package chat.rocket.android.chatrooms.di
import chat.rocket.android.chatrooms.ui.ChatRoomsFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class ChatRoomsFragmentProvider {
@ContributesAndroidInjector(modules = [ChatRoomsFragmentModule::class])
abstract fun provideChatRoomsFragment(): ChatRoomsFragment
}
\ No newline at end of file
package chat.rocket.android.chatrooms.presentation
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.util.launchUI
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.chatRooms
import chat.rocket.core.model.ChatRoom
import javax.inject.Inject
class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, private val strategy: CancelStrategy) {
@Inject lateinit var client: RocketChatClient
fun chatRooms() {
launchUI(strategy) {
view.showLoading()
val chatRooms = client.chatRooms().update
val openChatRooms = getOpenChatRooms(chatRooms)
val sortedOpenChatRooms = sortChatRooms(openChatRooms)
view.showChatRooms(sortedOpenChatRooms.toMutableList())
view.hideLoading()
}
}
private fun getOpenChatRooms(chatRooms: List<ChatRoom>): List<ChatRoom> {
return chatRooms.filter(ChatRoom::open)
}
private fun sortChatRooms(chatRooms: List<ChatRoom>): List<ChatRoom> {
return chatRooms.sortedByDescending {
chatRoom -> chatRoom.lastMessage?.timestamp
}
}
}
\ No newline at end of file
package chat.rocket.android.chatrooms.presentation
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.core.model.ChatRoom
interface ChatRoomsView : LoadingView, MessageView {
/**
* Shows the chat rooms.
*
* @param dataSet The data set to show.
*/
fun showChatRooms(dataSet: MutableList<ChatRoom>)
}
\ No newline at end of file
package chat.rocket.android.app.chatlist package chat.rocket.android.chatrooms.ui
import DateTimeHelper import DateTimeHelper
import DrawableHelper import DrawableHelper
import android.content.Context import android.content.Context
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.util.inflate
import chat.rocket.android.util.setVisibility
import chat.rocket.android.util.textContent
import chat.rocket.common.model.BaseRoom.RoomType
import chat.rocket.core.model.ChatRoom
import com.facebook.drawee.view.SimpleDraweeView import com.facebook.drawee.view.SimpleDraweeView
import kotlinx.android.synthetic.main.item_chat.view.* import kotlinx.android.synthetic.main.item_chat.view.*
class ChatListAdapter(private var dataSet: MutableList<Chat>, private val context: Context) : RecyclerView.Adapter<ChatListAdapter.ViewHolder>() { class ChatRoomsAdapter(private var dataSet: MutableList<ChatRoom>, private val context: Context) : RecyclerView.Adapter<ChatRoomsAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = ViewHolder(parent.inflate(R.layout.item_chat))
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_chat, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val chat = dataSet[position] val chatRoom = dataSet[position]
val chatRoomName = chatRoom.name
holder.userAvatar.setImageURI(chat.user.avatarUri) holder.chatName.textContent = chatRoomName
holder.chatName.text = chat.name
holder.lastMessage.text = chat.lastMessage
holder.lastMessageDateTime.text = DateTimeHelper.getDate(chat.lastMessageDateTime, context)
when (chat.type) { if (chatRoom.type == RoomType.ONE_TO_ONE) {
"p" -> DrawableHelper.compoundDrawable(holder.chatName, DrawableHelper.getDrawableFromId(R.drawable.ic_lock_outline_black, context)) // TODO Check the best way to get the current server url.
"c" -> DrawableHelper.compoundDrawable(holder.chatName, DrawableHelper.getDrawableFromId(R.drawable.ic_hashtag_black, context)) val canonicalUrl = chatRoom.client.restUrl.toString()
"d" -> DrawableHelper.compoundDrawable(holder.chatName, DrawableHelper.getUserStatusDrawable(chat.user.status, context)) holder.userAvatar.setImageURI(UrlHelper.getAvatarUrl(canonicalUrl, chatRoomName))
holder.userAvatar.setVisibility(true)
} else {
holder.roomAvatar.setImageDrawable(DrawableHelper.getTextDrawable(chatRoomName))
holder.roomAvatar.setVisibility(true)
} }
val totalUnreadMessage = chat.totalUnreadMessages val totalUnreadMessage = chatRoom.unread
when { when {
totalUnreadMessage in 1..99 -> { totalUnreadMessage in 1..99 -> {
holder.unreadMessage.text = totalUnreadMessage.toString() holder.unreadMessage.textContent = totalUnreadMessage.toString()
holder.unreadMessage.visibility = View.VISIBLE holder.unreadMessage.setVisibility(true)
} }
totalUnreadMessage > 99 -> { totalUnreadMessage > 99 -> {
holder.unreadMessage.text = context.getString(R.string.msg_more_than_ninety_nine_unread_messages) holder.unreadMessage.textContent = context.getString(R.string.msg_more_than_ninety_nine_unread_messages)
holder.unreadMessage.visibility = View.VISIBLE holder.unreadMessage.setVisibility(true)
}
}
val lastMessage = chatRoom.lastMessage
val lastMessageSender = lastMessage?.sender
if (lastMessage != null && lastMessageSender != null) {
val message = lastMessage.message
val senderUsername = lastMessageSender.username
when (senderUsername) {
chatRoomName -> {
holder.lastMessage.textContent = message
} }
// TODO Change to MySelf
// chatRoom.user?.username -> {
// holder.lastMessage.textContent = context.getString(R.string.msg_you) + ": $message"
// }
else -> {
holder.lastMessage.textContent = "@$senderUsername: $message"
}
}
val localDateTime = DateTimeHelper.getLocalDateTime(lastMessage.timestamp)
holder.lastMessageDateTime.textContent = DateTimeHelper.getDate(localDateTime, context)
} }
} }
override fun getItemCount(): Int = dataSet.size override fun getItemCount(): Int = dataSet.size
override fun getItemViewType(position: Int): Int = position
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val userAvatar: SimpleDraweeView = itemView.image_user_avatar val userAvatar: SimpleDraweeView = itemView.image_user_avatar
val roomAvatar: ImageView = itemView.image_room_avatar
val chatName: TextView = itemView.text_chat_name val chatName: TextView = itemView.text_chat_name
val lastMessage: TextView = itemView.text_last_message val lastMessage: TextView = itemView.text_last_message
val lastMessageDateTime: TextView = itemView.text_last_message_date_time val lastMessageDateTime: TextView = itemView.text_last_message_date_time
......
package chat.rocket.android.chatrooms.ui
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.widget.LinearLayoutManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import chat.rocket.android.R
import chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter
import chat.rocket.android.chatrooms.presentation.ChatRoomsView
import chat.rocket.android.widget.DividerItemDecoration
import chat.rocket.core.model.ChatRoom
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_chat_rooms.*
import javax.inject.Inject
class ChatRoomsFragment : Fragment(), ChatRoomsView {
@Inject lateinit var presenter: ChatRoomsPresenter
companion object {
fun newInstance() = ChatRoomsFragment()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_chat_rooms, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter.chatRooms()
}
override fun showChatRooms(dataSet: MutableList<ChatRoom>) {
activity?.apply {
recycler_view.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
recycler_view.addItemDecoration(DividerItemDecoration(this, 144, 32))
recycler_view.adapter = ChatRoomsAdapter(dataSet, this)
}
}
override fun showLoading() = view_loading.show()
override fun hideLoading() = view_loading.hide()
override fun showMessage(message: String) = Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
}
\ No newline at end of file
package chat.rocket.android.chatrooms.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.util.addFragment
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector
import javax.inject.Inject
class MainActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
AndroidInjection.inject(this)
addFragment("ChatRoomsFragment", R.id.fragment_container) {
ChatRoomsFragment.newInstance()
}
}
override fun supportFragmentInjector(): AndroidInjector<Fragment> {
return fragmentDispatchingAndroidInjector
}
}
\ No newline at end of file
package chat.rocket.android.core.behaviours
interface InternetView {
fun showNoInternetConnection()
}
\ No newline at end of file
package chat.rocket.android.core.behaviours package chat.rocket.android.core.behaviours
interface LoadingView { interface LoadingView {
fun showLoading() fun showLoading()
fun hideLoading() fun hideLoading()
} }
\ No newline at end of file
package chat.rocket.android.core.behaviours
interface MessageView {
fun showMessage(message: String)
fun showGenericErrorMessage()
}
\ No newline at end of file
...@@ -8,6 +8,7 @@ import kotlinx.coroutines.experimental.Job ...@@ -8,6 +8,7 @@ import kotlinx.coroutines.experimental.Job
import javax.inject.Inject import javax.inject.Inject
class CancelStrategy @Inject constructor(owner: LifecycleOwner, val jobs: Job) : LifecycleObserver { class CancelStrategy @Inject constructor(owner: LifecycleOwner, val jobs: Job) : LifecycleObserver {
init { init {
owner.lifecycle.addObserver(this) owner.lifecycle.addObserver(this)
} }
......
...@@ -10,7 +10,7 @@ import dagger.android.support.AndroidSupportInjectionModule ...@@ -10,7 +10,7 @@ import dagger.android.support.AndroidSupportInjectionModule
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
@Component(modules = arrayOf(AndroidSupportInjectionModule::class, AppModule::class, ActivityBuilder::class)) @Component(modules = [AndroidSupportInjectionModule::class, AppModule::class, ActivityBuilder::class])
interface AppComponent { interface AppComponent {
@Component.Builder @Component.Builder
......
package chat.rocket.android.dagger.module package chat.rocket.android.dagger.module
import chat.rocket.android.app.MainActivity import chat.rocket.android.authentication.di.AuthenticationModule
import chat.rocket.android.authentication.di.*
import chat.rocket.android.authentication.login.di.LoginFragmentProvider import chat.rocket.android.authentication.login.di.LoginFragmentProvider
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
import chat.rocket.android.authentication.ui.AuthenticationActivity import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.chatrooms.di.ChatRoomsFragmentProvider
import chat.rocket.android.chatrooms.ui.MainActivity
import chat.rocket.android.dagger.scope.PerActivity import chat.rocket.android.dagger.scope.PerActivity
import dagger.Module import dagger.Module
import dagger.android.ContributesAndroidInjector import dagger.android.ContributesAndroidInjector
...@@ -15,15 +16,14 @@ import dagger.android.ContributesAndroidInjector ...@@ -15,15 +16,14 @@ import dagger.android.ContributesAndroidInjector
abstract class ActivityBuilder { abstract class ActivityBuilder {
@PerActivity @PerActivity
@ContributesAndroidInjector(modules = arrayOf( @ContributesAndroidInjector(modules = [AuthenticationModule::class,
AuthenticationModule::class,
LoginFragmentProvider::class,
ServerFragmentProvider::class, ServerFragmentProvider::class,
LoginFragmentProvider::class,
SignupFragmentProvider::class, SignupFragmentProvider::class,
TwoFAFragmentProvider::class TwoFAFragmentProvider::class
)) ])
abstract fun bindAuthenticationActivity(): AuthenticationActivity abstract fun bindAuthenticationActivity(): AuthenticationActivity
@ContributesAndroidInjector @ContributesAndroidInjector(modules = [ChatRoomsFragmentProvider::class])
abstract fun bindMainActivity(): MainActivity abstract fun bindMainActivity(): MainActivity
} }
\ No newline at end of file
...@@ -5,11 +5,14 @@ import android.arch.persistence.room.Room ...@@ -5,11 +5,14 @@ import android.arch.persistence.room.Room
import android.content.Context import android.content.Context
import chat.rocket.android.BuildConfig import chat.rocket.android.BuildConfig
import chat.rocket.android.app.RocketChatDatabase import chat.rocket.android.app.RocketChatDatabase
import chat.rocket.android.authentication.infraestructure.AuthTokenRepository
import chat.rocket.android.server.infraestructure.ServerDao import chat.rocket.android.server.infraestructure.ServerDao
import chat.rocket.android.util.TimberLogger import chat.rocket.android.util.TimberLogger
import chat.rocket.common.util.PlatformLogger import chat.rocket.common.util.PlatformLogger
import chat.rocket.core.RocketChatClient
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import okhttp3.HttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import javax.inject.Singleton import javax.inject.Singleton
...@@ -17,6 +20,20 @@ import javax.inject.Singleton ...@@ -17,6 +20,20 @@ import javax.inject.Singleton
@Module @Module
class AppModule { class AppModule {
@Provides
@Singleton
fun provideRocketChatClient(okHttpClient: OkHttpClient, repository: AuthTokenRepository, logger: PlatformLogger): RocketChatClient {
return RocketChatClient.create {
httpClient = okHttpClient
tokenRepository = repository
platformLogger = logger
// TODO remove
restUrl = HttpUrl.parse("https://open.rocket.chat")!!
websocketUrl = "https://open.rocket.chat"
}
}
@Provides @Provides
@Singleton @Singleton
fun provideRocketChatDatabase(context: Application): RocketChatDatabase { fun provideRocketChatDatabase(context: Application): RocketChatDatabase {
...@@ -35,20 +52,6 @@ class AppModule { ...@@ -35,20 +52,6 @@ class AppModule {
return database.serverDao() return database.serverDao()
} }
/*@Provides
@Singleton
@IntoSet
fun provideHttpLoggingInterceptor(): Interceptor {
val interceptor = HttpLoggingInterceptor()
if (BuildConfig.DEBUG) {
interceptor.level = HttpLoggingInterceptor.Level.BODY
} else {
interceptor.level = HttpLoggingInterceptor.Level.HEADERS
}
return interceptor
}*/
@Provides @Provides
@Singleton @Singleton
fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor { fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor {
...@@ -70,9 +73,15 @@ class AppModule { ...@@ -70,9 +73,15 @@ class AppModule {
}.build() }.build()
} }
@Provides
@Singleton
fun provideAuthTokenRepository(): AuthTokenRepository {
return AuthTokenRepository()
}
@Provides @Provides
@Singleton @Singleton
fun providePlatformLogger(): PlatformLogger { fun providePlatformLogger(): PlatformLogger {
return TimberLogger() return TimberLogger
} }
} }
package chat.rocket.android.helper
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import android.view.View
object AnimationHelper {
/**
* Shakes a view.
*/
fun shakeView(viewToShake: View, x: Float = 2F, num: Int = 0) {
if (num == 6) {
viewToShake.translationX = 0.toFloat()
return
}
val animatorSet = AnimatorSet()
animatorSet.playTogether(ObjectAnimator.ofFloat(viewToShake, "translationX", dp(viewToShake.context, x)))
animatorSet.duration = 50
animatorSet.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
shakeView(viewToShake, if (num == 5) 0.toFloat() else -x, num + 1)
}
})
animatorSet.start()
}
/**
* Vibrates the smart phone.
*/
fun vibrateSmartPhone(context: Context) {
val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
if (Build.VERSION.SDK_INT >= 26) {
vibrator.vibrate(VibrationEffect.createOneShot(200, VibrationEffect.DEFAULT_AMPLITUDE))
} else {
vibrator.vibrate(200)
}
}
private fun dp(context: Context, value: Float): Float {
val density = context.resources.displayMetrics.density
val result = Math.ceil(density.times(value.toDouble()))
return result.toFloat()
}
}
\ No newline at end of file
package chat.rocket.android.app package chat.rocket.android.helper
import android.graphics.Rect import android.graphics.Rect
import android.view.View import android.view.View
......
package chat.rocket.android.helper
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.run
import java.io.IOException
import java.net.InetSocketAddress
import java.net.Socket
object NetworkHelper {
/**
* Checks whether there is internet access.
*
* The original author of this code is Levit and you can see his answer here: https://stackoverflow.com/a/27312494/4744263
*
* @return true if there is internet access, false otherwise.
*/
suspend fun hasInternetAccess(): Boolean = run(CommonPool) {
try {
val socket = Socket()
val inetSocketAddress = InetSocketAddress("8.8.8.8", 53)
socket.connect(inetSocketAddress, 1500)
socket.close()
true
} catch (e: IOException) {
false
}
}
}
\ No newline at end of file
package chat.rocket.android.helper
import android.text.SpannableString
import android.text.Spanned
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.widget.TextView
import chat.rocket.android.util.ifEmpty
object TextHelper {
/**
* Adds clickable link(s) to a TextView.
*
* @param textView The TextView to add clickable links.
* @param links The TextView content(s) to act as clickable links.
* @param clickableSpans The ClickableSpan(s) to handle the onClick listener.
*/
fun addLink(textView: TextView, links: Array<String>, clickableSpans: Array<ClickableSpan>) {
val spannableString = SpannableString(textView.text)
for (i in links.indices) {
val clickableSpan = clickableSpans[i]
val link = links[i]
val startIndexOfLink = textView.text.indexOf(link)
spannableString.setSpan(clickableSpan, startIndexOfLink, startIndexOfLink + link.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
textView.movementMethod = LinkMovementMethod.getInstance()
textView.setText(spannableString, TextView.BufferType.SPANNABLE)
}
/**
* Returns the first character from a string.
*
* @param string The string to get its first character.
* @return The first character from a string.
*/
fun getFirstCharacter(string: String): String {
string.ifEmpty("?")
return string.substring(0, 1).toUpperCase()
}
}
\ No newline at end of file
package chat.rocket.android.helper
object UrlHelper {
/**
* Returns the avatar URL.
*
* @param serverUrl The serverUrl.
* @param chatRoomName The chat room name.
* @return The avatar URL.
*/
fun getAvatarUrl(serverUrl: String, chatRoomName: String): String = serverUrl + "avatar/" + chatRoomName
}
\ No newline at end of file
package chat.rocket.android.server.infraestructure;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Delete;
import android.arch.persistence.room.Insert;
import android.arch.persistence.room.OnConflictStrategy;
import android.arch.persistence.room.Query;
import android.arch.persistence.room.Update;
import java.util.List;
import io.reactivex.Single;
@Dao
public interface ServerDao {
@Insert(onConflict = OnConflictStrategy.FAIL)
void insertServer(ServerEntity serverEntity);
@Update
void updateServer(ServerEntity serverEntity);
@Delete
void deleteServer(ServerEntity serverEntity);
@Query("SELECT * FROM server")
Single<List<ServerEntity>> getServers();
@Query("SELECT * FROM server WHERE id = :serverId")
Single<ServerEntity> getServer(Long serverId);
}
package chat.rocket.android.server.infraestructure
import android.arch.persistence.room.*
import io.reactivex.Single
@Dao
interface ServerDao {
@Query("SELECT * FROM server")
fun getServers(): Single<List<ServerEntity>>
@Insert(onConflict = OnConflictStrategy.FAIL)
fun insertServer(serverEntity: ServerEntity)
@Update
fun updateServer(serverEntity: ServerEntity)
@Delete
fun deleteServer(serverEntity: ServerEntity)
@Query("SELECT * FROM server WHERE id = :serverId")
fun getServer(serverId: Long?): Single<ServerEntity>
}
...@@ -2,13 +2,20 @@ package chat.rocket.android.util ...@@ -2,13 +2,20 @@ package chat.rocket.android.util
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import chat.rocket.android.R
fun AppCompatActivity.addFragment(tag: String, layoutId: Int, newInstance: () -> Fragment) { fun AppCompatActivity.addFragment(tag: String, layoutId: Int, newInstance: () -> Fragment) {
val fragment = supportFragmentManager.findFragmentByTag(tag) ?: newInstance() val fragment = supportFragmentManager.findFragmentByTag(tag) ?: newInstance()
supportFragmentManager.beginTransaction().replace(layoutId, fragment, tag).commit() supportFragmentManager.beginTransaction()
.replace(layoutId, fragment, tag)
.commit()
} }
fun AppCompatActivity.addFragmentBackStack(tag: String, layoutId: Int, newInstance: () -> Fragment) { fun AppCompatActivity.addFragmentBackStack(tag: String, layoutId: Int, newInstance: () -> Fragment) {
val fragment = supportFragmentManager.findFragmentByTag(tag) ?: newInstance() val fragment = supportFragmentManager.findFragmentByTag(tag) ?: newInstance()
supportFragmentManager.beginTransaction().replace(layoutId, fragment, tag).addToBackStack(tag).commit() supportFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right)
.replace(layoutId, fragment, tag)
.addToBackStack(tag)
.commit()
} }
\ No newline at end of file
package chat.rocket.android.util package chat.rocket.android.util
import android.support.annotation.LayoutRes
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
fun String.ifEmpty(value: String): String { fun String.ifEmpty(value: String): String {
...@@ -9,6 +13,26 @@ fun String.ifEmpty(value: String): String { ...@@ -9,6 +13,26 @@ fun String.ifEmpty(value: String): String {
return this return this
} }
var TextView.content: String fun View.setVisibility(value: Boolean) {
get() = this.text.toString() visibility = if (value) {
set(value) { this.text = value } View.VISIBLE
\ No newline at end of file } else {
View.GONE
}
}
fun ViewGroup.inflate(@LayoutRes resource: Int): View {
return LayoutInflater.from(context).inflate(resource, this, false)
}
var TextView.textContent: String
get() = text.toString()
set(value) {
text = value
}
var TextView.hintContent: String
get() = hint.toString()
set(value) {
hint = value
}
...@@ -3,7 +3,8 @@ package chat.rocket.android.util ...@@ -3,7 +3,8 @@ package chat.rocket.android.util
import chat.rocket.common.util.PlatformLogger import chat.rocket.common.util.PlatformLogger
import timber.log.Timber import timber.log.Timber
class TimberLogger : PlatformLogger { object TimberLogger : PlatformLogger {
override fun debug(s: String) { override fun debug(s: String) {
Timber.d(s) Timber.d(s)
} }
......
package chat.rocket.android.webview
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.webkit.WebView
import android.webkit.WebViewClient
import chat.rocket.android.R
import kotlinx.android.synthetic.main.activity_web_view.*
import kotlinx.android.synthetic.main.app_bar.*
fun Context.webViewIntent(webPageUrl: String): Intent {
return Intent(this, WebViewActivity::class.java).apply {
putExtra(INTENT_WEB_PAGE_URL, webPageUrl)
}
}
private const val INTENT_WEB_PAGE_URL = "web_page_url"
class WebViewActivity : AppCompatActivity() {
private lateinit var webPageUrl: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_web_view)
webPageUrl = intent.getStringExtra(INTENT_WEB_PAGE_URL)
requireNotNull(webPageUrl) { "no web_page_url provided in Intent extras" }
setupToolbar()
}
override fun onResume() {
super.onResume()
setupWebView()
}
override fun onBackPressed() {
if (web_view.canGoBack()) {
web_view.goBack()
} else {
finishActivity()
}
}
private fun setupToolbar() {
toolbar.title = getString(R.string.title_legal_terms)
toolbar.setNavigationIcon(R.drawable.ic_close_white_24dp)
toolbar.setNavigationOnClickListener {
finishActivity()
}
}
@SuppressLint("SetJavaScriptEnabled")
private fun setupWebView() {
web_view.settings.javaScriptEnabled = true
web_view.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
view_loading.hide()
}
}
web_view.loadUrl(webPageUrl)
}
private fun finishActivity() {
super.onBackPressed()
overridePendingTransition(R.anim.hold, R.anim.slide_down)
}
}
\ No newline at end of file
package chat.rocket.android.widget
import android.content.Context
import android.graphics.Canvas
import android.graphics.drawable.Drawable
import android.support.annotation.DrawableRes
import android.support.v4.content.ContextCompat
import android.support.v7.widget.RecyclerView
/**
* Adds a default or custom divider to specific item views from the adapter's data set.
* @see RecyclerView.ItemDecoration
*/
class DividerItemDecoration() : RecyclerView.ItemDecoration() {
private lateinit var divider: Drawable
private var boundStart = 0
private var boundRight = 0
// Default divider will be used.
constructor(context: Context) : this() {
val attrs = intArrayOf(android.R.attr.listDivider)
val styledAttributes = context.obtainStyledAttributes(attrs)
divider = styledAttributes.getDrawable(0)
styledAttributes.recycle()
}
// Default divider with custom boundaries (start and right) will be used.
constructor(context: Context, boundStart: Int, boundRight: Int) : this() {
val attrs = intArrayOf(android.R.attr.listDivider)
val styledAttributes = context.obtainStyledAttributes(attrs)
divider = styledAttributes.getDrawable(0)
styledAttributes.recycle()
this.boundStart = boundStart
this.boundRight = boundRight
}
// Custom divider will be used.
constructor(context: Context, @DrawableRes drawableResId: Int) : this() {
val customDrawable = ContextCompat.getDrawable(context, drawableResId)
if (customDrawable != null) {
divider = customDrawable
}
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val left = parent.paddingLeft + boundStart
val right = (parent.width - parent.paddingRight) - boundRight
val childCount = parent.childCount
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
val params = child.layoutParams as RecyclerView.LayoutParams
val top = child.bottom + params.bottomMargin
val bottom = top + divider.intrinsicHeight
divider.setBounds(left, top, right, bottom)
divider.draw(c)
}
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<translate
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="-100%"
android:fromYDelta="0%"
android:toXDelta="0%"
android:toYDelta="0%" />
</set>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<translate
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="100%"
android:fromYDelta="0%"
android:toXDelta="0%"
android:toYDelta="0%" />
</set>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<translate
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="0%"
android:fromYDelta="0%"
android:toXDelta="-100%"
android:toYDelta="0%" />
</set>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<translate
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="0%"
android:fromYDelta="0%"
android:toXDelta="100%"
android:toYDelta="0%" />
</set>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="800"
android:fromYDelta="0.0%p"
android:toYDelta="0.0%p" />
</set>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="350"
android:fromYDelta="0.0%"
android:toYDelta="100.0%" />
</set>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="350"
android:fromYDelta="100.0%"
android:toYDelta="0.0%" />
</set>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>
<?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=".webview.WebViewActivity">
<include
android:id="@+id/layout_app_bar"
layout="@layout/app_bar" />
<WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/layout_app_bar" />
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
app:indicatorColor="@color/black"
app:indicatorName="BallPulseIndicator" />
</RelativeLayout>
\ No newline at end of file
...@@ -5,8 +5,8 @@ ...@@ -5,8 +5,8 @@
android:id="@+id/scroll_view" android:id="@+id/scroll_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:animateLayoutChanges="true" android:fillViewport="true"
android:fillViewport="true"> tools:context=".authentication.login.ui.LoginFragment">
<android.support.constraint.ConstraintLayout <android.support.constraint.ConstraintLayout
android:id="@+id/middle_container" android:id="@+id/middle_container"
...@@ -45,14 +45,40 @@ ...@@ -45,14 +45,40 @@
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_username_or_email" /> app:layout_constraintTop_toBottomOf="@+id/text_username_or_email" />
<TextView
android:id="@+id/text_new_to_rocket_chat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins"
android:layout_marginStart="@dimen/screen_edge_left_and_right_margins"
android:layout_marginTop="16dp"
android:gravity="center"
android:textColorLink="@color/colorAccent"
android:visibility="gone"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_password"
tools:visibility="visible" />
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:visibility="gone"
app:indicatorName="BallPulseIndicator"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_password"
tools:visibility="visible" />
<LinearLayout <LinearLayout
android:id="@+id/social_accounts_container" android:id="@+id/social_accounts_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins" android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins"
android:layout_marginStart="@dimen/screen_edge_left_and_right_margins" android:layout_marginStart="@dimen/screen_edge_left_and_right_margins"
android:layout_marginTop="16dp" android:layout_marginTop="20dp"
android:animateLayoutChanges="true"
android:background="@color/colorPrimaryDark" android:background="@color/colorPrimaryDark"
android:gravity="center" android:gravity="center"
android:orientation="vertical" android:orientation="vertical"
...@@ -61,7 +87,7 @@ ...@@ -61,7 +87,7 @@
android:visibility="gone" android:visibility="gone"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_password" app:layout_constraintTop_toBottomOf="@+id/text_new_to_rocket_chat"
tools:visibility="visible"> tools:visibility="visible">
<TextView <TextView
...@@ -165,23 +191,6 @@ ...@@ -165,23 +191,6 @@
app:layout_constraintTop_toBottomOf="@+id/social_accounts_container" app:layout_constraintTop_toBottomOf="@+id/social_accounts_container"
tools:visibility="visible" /> tools:visibility="visible" />
<TextView
android:id="@+id/text_new_to_rocket_chat"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginBottom="16dp"
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins"
android:layout_marginStart="@dimen/screen_edge_left_and_right_margins"
android:layout_marginTop="16dp"
android:gravity="center"
android:text="@string/msg_new_to_rocket_chat"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/social_accounts_container"
tools:visibility="visible" />
<Button <Button
android:id="@+id/button_log_in" android:id="@+id/button_log_in"
style="@style/AuthenticationButton" style="@style/AuthenticationButton"
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <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:id="@+id/relative_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:focusableInTouchMode="true"
tools:context=".authentication.server.ui.ServerFragment">
<TextView <TextView
android:id="@+id/text_headline" android:id="@+id/text_headline"
...@@ -10,7 +15,7 @@ ...@@ -10,7 +15,7 @@
android:text="@string/title_sign_in_your_server" /> android:text="@string/title_sign_in_your_server" />
<TextView <TextView
android:id="@+id/server_protocol_label" android:id="@+id/text_server_protocol"
style="@style/AuthenticationLabel" style="@style/AuthenticationLabel"
android:layout_below="@id/text_headline" android:layout_below="@id/text_headline"
android:layout_marginTop="32dp" android:layout_marginTop="32dp"
...@@ -23,12 +28,23 @@ ...@@ -23,12 +28,23 @@
android:layout_below="@id/text_headline" android:layout_below="@id/text_headline"
android:layout_marginStart="0dp" android:layout_marginStart="0dp"
android:layout_marginTop="32dp" android:layout_marginTop="32dp"
android:layout_toEndOf="@id/server_protocol_label" android:layout_toEndOf="@id/text_server_protocol"
android:cursorVisible="false"
android:hint="@string/default_server" android:hint="@string/default_server"
android:imeOptions="actionDone" android:imeOptions="actionDone"
android:inputType="textUri" android:inputType="textUri"
android:paddingEnd="0dp"
android:paddingStart="0dp" /> android:paddingStart="0dp" />
<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 <Button
android:id="@+id/button_connect" android:id="@+id/button_connect"
style="@style/AuthenticationButton" style="@style/AuthenticationButton"
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/constraint_layout" android:id="@+id/constraint_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
tools:context=".authentication.signup.ui.SignupFragment">
<TextView <TextView
android:id="@+id/text_headline" android:id="@+id/text_headline"
...@@ -61,6 +63,19 @@ ...@@ -61,6 +63,19 @@
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_password" /> app:layout_constraintTop_toBottomOf="@+id/text_password" />
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginTop="16dp"
android:visibility="gone"
app:indicatorName="BallPulseIndicator"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_email"
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/text_new_user_agreement" android:id="@+id/text_new_user_agreement"
android:layout_width="wrap_content" android:layout_width="wrap_content"
...@@ -69,7 +84,7 @@ ...@@ -69,7 +84,7 @@
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins" android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins"
android:layout_marginStart="@dimen/screen_edge_left_and_right_margins" android:layout_marginStart="@dimen/screen_edge_left_and_right_margins"
android:gravity="center" android:gravity="center"
android:text="@string/msg_new_user_agreement" android:textColorLink="@color/colorAccent"
app:layout_constraintBottom_toTopOf="@+id/button_sign_up" app:layout_constraintBottom_toTopOf="@+id/button_sign_up"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" /> app:layout_constraintRight_toRightOf="parent" />
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <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_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
tools:context=".authentication.twofactor.ui.TwoFAFragment">
<TextView <TextView
android:id="@+id/text_headline" android:id="@+id/text_headline"
...@@ -19,6 +22,16 @@ ...@@ -19,6 +22,16 @@
android:imeOptions="actionDone" android:imeOptions="actionDone"
android:inputType="text" /> 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 <Button
android:id="@+id/button_log_in" android:id="@+id/button_log_in"
style="@style/AuthenticationButton" style="@style/AuthenticationButton"
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
tools:context=".chatrooms.ui.ChatRoomsFragment">
<android.support.v7.widget.RecyclerView <android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view" android:id="@+id/recycler_view"
...@@ -24,4 +26,14 @@ ...@@ -24,4 +26,14 @@
app:floatingSearch_searchBarMarginRight="4dp" app:floatingSearch_searchBarMarginRight="4dp"
app:floatingSearch_searchBarMarginTop="28dp" /> app:floatingSearch_searchBarMarginTop="28dp" />
<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:indicatorColor="@color/black"
app:indicatorName="BallPulseIndicator"
tools:visibility="visible" />
</RelativeLayout> </RelativeLayout>
\ No newline at end of file
...@@ -4,28 +4,42 @@ ...@@ -4,28 +4,42 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp" android:layout_marginBottom="12dp"
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins" android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins"
android:layout_marginStart="@dimen/screen_edge_left_and_right_margins" android:layout_marginStart="@dimen/screen_edge_left_and_right_margins"
android:layout_marginTop="16dp"> android:layout_marginTop="12dp">
<android.support.constraint.ConstraintLayout
android:id="@+id/avatar_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/middle_container"
app:layout_constraintTop_toTopOf="parent">
<com.facebook.drawee.view.SimpleDraweeView <com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_user_avatar" android:id="@+id/image_user_avatar"
android:layout_width="40dp" android:layout_width="40dp"
android:layout_height="40dp" android:layout_height="40dp"
app:layout_constraintBottom_toBottomOf="parent" android:visibility="gone"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/middle_container"
app:layout_constraintTop_toTopOf="parent"
app:roundAsCircle="true" /> app:roundAsCircle="true" />
<ImageView
android:id="@+id/image_room_avatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:visibility="gone"
tools:ignore="contentDescription" />
</android.support.constraint.ConstraintLayout>
<android.support.constraint.ConstraintLayout <android.support.constraint.ConstraintLayout
android:id="@+id/middle_container" android:id="@+id/middle_container"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
app:layout_constraintLeft_toRightOf="@+id/image_user_avatar" app:layout_constraintLeft_toRightOf="@+id/avatar_container"
app:layout_constraintRight_toLeftOf="@+id/right_container"> app:layout_constraintRight_toLeftOf="@+id/right_container">
<TextView <TextView
...@@ -36,17 +50,16 @@ ...@@ -36,17 +50,16 @@
android:drawablePadding="5dp" android:drawablePadding="5dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
tools:text="Ronald Perkins" /> tools:text="General" />
<TextView <TextView
android:id="@+id/text_last_message" android:id="@+id/text_last_message"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="2"
app:layout_constraintTop_toBottomOf="@+id/text_chat_name" app:layout_constraintTop_toBottomOf="@+id/text_chat_name"
tools:text="Type something" /> tools:text="You: Type something" />
</android.support.constraint.ConstraintLayout> </android.support.constraint.ConstraintLayout>
<android.support.constraint.ConstraintLayout <android.support.constraint.ConstraintLayout
......
<?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- 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_sign_up">Inscreva-se</string> <string name="title_sign_up">Inscreva-se</string>
<string name="title_legal_terms">Termos Legais</string>
<!-- Actions --> <!-- Actions -->
<string name="action_connect">Conectar</string> <string name="action_connect">Conectar</string>
<string name="action_send">Enviar</string> <string name="action_send">Enviar</string>
<string name="action_terms_of_service">Termos de Serviço</string>
<string name="action_privacy_policy">Política de Privacidade</string>
<!-- Regular information messages --> <!-- Regular information messages -->
<string name="msg_no_internet_connection">Sem conexão à internet</string>
<string name="msg_generic_error">Desculpe, ocorreu um erro, tente novamente</string>
<string name="msg_username">nome de usuário</string> <string name="msg_username">nome de usuário</string>
<string name="msg_username_or_email">nome de usuário ou email</string> <string name="msg_username_or_email">nome de usuário ou email</string>
<string name="msg_password">senha</string> <string name="msg_password">senha</string>
<string name="msg_name_and_surname">nome e sobrenome</string> <string name="msg_name_and_surname">nome e sobrenome</string>
<string name="msg_email">email</string> <string name="msg_email">email</string>
<string name="msg_or_continue_using_social_accounts">Ou continue através de contas sociais</string> <string name="msg_or_continue_using_social_accounts">Ou continue através de contas sociais</string>
<string name="msg_new_to_rocket_chat">Novo no Rocket Chat? <font color='#FF1976D2'>Inscreva-se</font></string> <string name="msg_new_to_rocket_chat">Novo no Rocket Chat? %1$s</string>
<string name="msg_new_user_agreement">Ao proceder você concorda com nossos\n<font color='#FF1976D2'>Termos de Serviço</font> e <font color='#FF1976D2'>Política de Privacidade</font></string> <string name="msg_new_user_agreement">Ao proceder você concorda com nossos %1$s e %2$s</string>
<string name="msg_2fa_code">Código 2FA</string> <string name="msg_2fa_code">Código 2FA</string>
<string name="msg_invalid_2fa_code">Código 2FA inválido</string>
<string name="msg_yesterday">ontem</string> <string name="msg_yesterday">ontem</string>
<string name="msg_message">Messagem</string> <string name="msg_message">Messagem</string>
<string name="msg_content_description_log_in_using_facebook">Fazer login através do Facebook</string> <string name="msg_content_description_log_in_using_facebook">Fazer login através do Facebook</string>
...@@ -29,6 +34,6 @@ ...@@ -29,6 +34,6 @@
<string name="msg_content_description_log_in_using_meteor">Fazer login através do Meteor</string> <string name="msg_content_description_log_in_using_meteor">Fazer login através do Meteor</string>
<string name="msg_content_description_log_in_using_twitter">Fazer login através do Twitter</string> <string name="msg_content_description_log_in_using_twitter">Fazer login através do Twitter</string>
<string name="msg_content_description_log_in_using_gitlab">Fazer login através do Gitlab</string> <string name="msg_content_description_log_in_using_gitlab">Fazer login através do Gitlab</string>
<string name="msg_content_description_chat_icon">Ícone do chat</string> <string name="msg_you">Você</string>
</resources> </resources>
...@@ -14,4 +14,6 @@ ...@@ -14,4 +14,6 @@
<color name="colorDividerMessageComposer">#D8D8D8</color> <color name="colorDividerMessageComposer">#D8D8D8</color>
<color name="white">#FFFFFFFF</color> <color name="white">#FFFFFFFF</color>
<color name="black">#FF000000</color>
</resources> </resources>
\ No newline at end of file
...@@ -5,21 +5,27 @@ ...@@ -5,21 +5,27 @@
<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_sign_up">Sign up</string> <string name="title_sign_up">Sign up</string>
<string name="title_legal_terms">Legal Terms</string>
<!-- Actions --> <!-- Actions -->
<string name="action_connect">Connect</string> <string name="action_connect">Connect</string>
<string name="action_send">Send</string> <string name="action_send">Send</string>
<string name="action_terms_of_service">Terms of Service</string>
<string name="action_privacy_policy">Privacy Policy</string>
<!-- Regular information messages --> <!-- Regular information messages -->
<string name="msg_no_internet_connection">No internet connection</string>
<string name="msg_generic_error">Sorry, an error has occurred, please try again</string>
<string name="msg_username">username</string> <string name="msg_username">username</string>
<string name="msg_username_or_email">username or email</string> <string name="msg_username_or_email">username or email</string>
<string name="msg_password">password</string> <string name="msg_password">password</string>
<string name="msg_name_and_surname">name and surname</string> <string name="msg_name_and_surname">name and surname</string>
<string name="msg_email">email</string> <string name="msg_email">email</string>
<string name="msg_or_continue_using_social_accounts">Or continue using social accounts</string> <string name="msg_or_continue_using_social_accounts">Or continue using social accounts</string>
<string name="msg_new_to_rocket_chat">New to Rocket Chat? <font color='#FF1976D2'>Sign up</font></string> <string name="msg_new_to_rocket_chat">New to Rocket Chat? %1$s</string>
<string name="msg_new_user_agreement">By proceeding you are agreeing to our\n<font color='#FF1976D2'>Terms of Service</font> and <font color='#FF1976D2'>Privacy Policy</font></string> <string name="msg_new_user_agreement">By proceeding you are agreeing to our\n%1$s and %2$s</string>
<string name="msg_2fa_code">2FA Code</string> <string name="msg_2fa_code">2FA Code</string>
<string name="msg_invalid_2fa_code">Invalid 2FA Code</string>
<string name="msg_more_than_ninety_nine_unread_messages" translatable="false">99+</string> <string name="msg_more_than_ninety_nine_unread_messages" translatable="false">99+</string>
<string name="msg_yesterday">Yesterday</string> <string name="msg_yesterday">Yesterday</string>
<string name="msg_message">Message</string> <string name="msg_message">Message</string>
...@@ -30,6 +36,6 @@ ...@@ -30,6 +36,6 @@
<string name="msg_content_description_log_in_using_meteor">Login using Meteor</string> <string name="msg_content_description_log_in_using_meteor">Login using Meteor</string>
<string name="msg_content_description_log_in_using_twitter">Login using Twitter</string> <string name="msg_content_description_log_in_using_twitter">Login using Twitter</string>
<string name="msg_content_description_log_in_using_gitlab">Login using Gitlab</string> <string name="msg_content_description_log_in_using_gitlab">Login using Gitlab</string>
<string name="msg_content_description_chat_icon">Chat icon</string> <string name="msg_you">You</string>
</resources> </resources>
\ No newline at end of file
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
</style> </style>
<style name="AuthenticationTheme" parent="Theme.AppCompat.NoActionBar"> <style name="AuthenticationTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:navigationBarColor">@color/colorPrimary</item> <item name="android:statusBarColor">@color/colorPrimaryDark</item>
</style> </style>
<style name="ChatListTheme" parent="AppTheme"> <style name="ChatListTheme" parent="AppTheme">
......
...@@ -8,7 +8,7 @@ buildscript { ...@@ -8,7 +8,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.0.1' classpath "com.android.tools.build:gradle:3.0.1"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}" classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}"
classpath 'com.google.gms:google-services:3.1.2' classpath 'com.google.gms:google-services:3.1.2'
...@@ -24,6 +24,7 @@ allprojects { ...@@ -24,6 +24,7 @@ allprojects {
jcenter() jcenter()
maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
maven { url "https://jitpack.io" } maven { url "https://jitpack.io" }
maven { url "http://dl.bintray.com/amulyakhare/maven" } // For TextDrawable.
} }
apply from: rootProject.file('dependencies.gradle') apply from: rootProject.file('dependencies.gradle')
......
...@@ -10,7 +10,7 @@ machine: ...@@ -10,7 +10,7 @@ machine:
GRADLE_OPTS: '-Xmx1024m -Dorg.gradle.jvmargs="-Xmx1024m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError"' GRADLE_OPTS: '-Xmx1024m -Dorg.gradle.jvmargs="-Xmx1024m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError"'
JAVA_OPTS: "-Xms518m -Xmx1024m" JAVA_OPTS: "-Xms518m -Xmx1024m"
pre: pre:
- git clone https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK.git Rocket.Chat.Kotlin.Sdk - git clone https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK.git Rocket.Chat.Kotlin.SDK
dependencies: dependencies:
pre: pre:
......
...@@ -7,7 +7,7 @@ ext { ...@@ -7,7 +7,7 @@ ext {
targetSdk : 27, targetSdk : 27,
buildTools : '27.0.0', buildTools : '27.0.0',
kotlin : '1.2.10', kotlin : '1.2.10',
coroutine : '0.20', coroutine : '0.21',
dokka : '0.9.15', dokka : '0.9.15',
kotshi : '0.3.0-beta2', kotshi : '0.3.0-beta2',
...@@ -23,10 +23,13 @@ ext { ...@@ -23,10 +23,13 @@ ext {
okhttp : '3.9.0', okhttp : '3.9.0',
timber : '4.5.1', timber : '4.5.1',
threeTenABP : '1.0.5', threeTenABP : '1.0.5',
fresco : '1.5.0', fresco : '1.7.1',
frescoImageViewer : '0.5.0', frescoImageViewer : '0.5.0',
floatingSearchView: '2.1.1', floatingSearchView : '2.1.1',
androidSvg : '1.2.1', androidSvg : '1.2.1',
aVLoadingIndicatorView : '2.1.3',
textDrawable : '1.0.2',
playServices : '11.8.0',
// For testing // For testing
junit : '4.12', junit : '4.12',
...@@ -61,6 +64,7 @@ ext { ...@@ -61,6 +64,7 @@ ext {
rxandroid : "io.reactivex.rxjava2:rxandroid:${versions.rxandroid}", rxandroid : "io.reactivex.rxjava2:rxandroid:${versions.rxandroid}",
moshi : "com.squareup.moshi:moshi:${versions.moshi}", moshi : "com.squareup.moshi:moshi:${versions.moshi}",
moshiKotlin : "com.squareup.moshi:moshi-kotlin:${versions.moshi}",
okhttp : "com.squareup.okhttp3:okhttp:${versions.okhttp}", okhttp : "com.squareup.okhttp3:okhttp:${versions.okhttp}",
okhttpLogger : "com.squareup.okhttp3:logging-interceptor:${versions.okhttp}", okhttpLogger : "com.squareup.okhttp3:logging-interceptor:${versions.okhttp}",
...@@ -78,6 +82,12 @@ ext { ...@@ -78,6 +82,12 @@ ext {
androidSvg : "com.caverock:androidsvg:${versions.androidSvg}", androidSvg : "com.caverock:androidsvg:${versions.androidSvg}",
aVLoadingIndicatorView : "com.wang.avi:library:${versions.aVLoadingIndicatorView}",
textDrawable : "com.github.rocketchat:textdrawable:${versions.textDrawable}",
playServicesGcm : "com.google.android.gms:play-services-gcm:${versions.playServices}",
// For testing // For testing
toolsJar : files(Jvm.current().getToolsJar()), toolsJar : files(Jvm.current().getToolsJar()),
junit : "junit:junit:$versions.junit", junit : "junit:junit:$versions.junit",
......
include ':app', 'common', 'core', ':player' include ':app', ':player'
\ No newline at end of file
project(':common').projectDir = new File(settingsDir, '../Rocket.Chat.Kotlin.SDK/common')
project(':core').projectDir = new File(settingsDir, '../Rocket.Chat.Kotlin.SDK/core')
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