Commit de96a48d authored by Aniket Singh's avatar Aniket Singh

Merge branch 'develop-2.x' into aniket/feat/create-new-channel

parents 15b6c490 30b3b8b0
version: 2
jobs:
build-kotlin-sdk:
docker:
- image: circleci/android:api-27-alpha
environment:
JVM_OPTS: -Xmx3200m
steps:
- checkout
- run:
name: checkout Rocket.Chat.Kotlin.SDK
command: git clone https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK.git ../Rocket.Chat.Kotlin.SDK
- run:
name: ANDROID_HOME
command: echo "sdk.dir="$ANDROID_HOME > local.properties
- run:
name: Build Kotlin.SDK
command: pushd app/ ; ./build-sdk.sh ; popd
- save_cache:
paths:
- ~/.gradle
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}-{{ checksum "player/build.gradle" }}
- save_cache:
paths:
- app/libs/
- ../Rocket.Chat.Kotlin.SDK/.last_commit_hash
key: kotlin-sdk-{{ .Revision }}
- store_artifacts:
path: app/libs/
destination: libs
code-analysis:
docker:
- image: circleci/android:api-27-alpha
environment:
JVM_OPTS: -Xmx3200m
steps:
- checkout
- run:
name: ANDROID_HOME
command: echo "sdk.dir="$ANDROID_HOME > local.properties
- run:
name: checkout Rocket.Chat.Kotlin.SDK
command: git clone https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK.git ../Rocket.Chat.Kotlin.SDK
- restore_cache:
key: kotlin-sdk-{{ .Revision }}
- restore_cache:
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}-{{ checksum "player/build.gradle" }}
- run:
name: Download Dependencies
command: ./gradlew androidDependencies --quiet --console=plain
- save_cache:
paths:
- ~/.gradle
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}-{{ checksum "player/build.gradle" }}
- run:
name: Run Lint #, Checkstyles, PMD, Findbugs...
command: ./gradlew lint
- run:
name: Run Unit test
command: echo ./gradlew test # TODO: Fix unit test errors soon...
- store_artifacts:
path: app/build/reports/
destination: reports
build-apk:
docker:
- image: circleci/android:api-27-alpha
environment:
JVM_OPTS: -Xmx3200m
steps:
- checkout
- run:
name: restore files from ENV
command: |
echo $ROCKET_JKS_BASE64 | base64 --decode > Rocket.jks
echo $ROCKET_PLAY_JSON | base64 --decode > app/rocket-chat.json
- run:
name: checkout Rocket.Chat.Kotlin.SDK
command: git clone https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK.git ../Rocket.Chat.Kotlin.SDK
- restore_cache:
key: kotlin-sdk-{{ .Revision }}
- restore_cache:
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}-{{ checksum "player/build.gradle" }}
- run:
name: Download Dependencies
command: ./gradlew androidDependencies --quiet --console=plain
- save_cache:
paths:
- ~/.gradle
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}-{{ checksum "player/build.gradle" }}
- run:
name: Build APK
command: |
./gradlew assembleRelease --quiet --console=plain --stacktrace
- store_artifacts:
path: app/build/outputs/apk
destination: apks
workflows:
version: 2
build-deploy:
jobs:
- build-kotlin-sdk
- code-analysis:
requires:
- build-kotlin-sdk
filters:
branches:
ignore: # skip on merge commits.
- develop
- develop-2.x
- master
- build-apk:
requires:
- build-kotlin-sdk
......@@ -3,6 +3,7 @@ apply plugin: 'io.fabric'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'realm-android'
android {
compileSdkVersion versions.compileSdk
......@@ -12,8 +13,8 @@ android {
applicationId "chat.rocket.android"
minSdkVersion 21
targetSdkVersion versions.targetSdk
versionCode 1011
versionName "2.0.0-dev9"
versionCode 2002
versionName "2.0.0-beta2"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}
......@@ -29,13 +30,16 @@ android {
buildTypes {
release {
buildConfigField "String", "REQUIRED_SERVER_VERSION", '"0.62.0"'
buildConfigField "String", "RECOMMENDED_SERVER_VERSION", '"0.63.0"'
signingConfig signingConfigs.release
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
applicationIdSuffix ".dev"
}
debug {
buildConfigField "String", "REQUIRED_SERVER_VERSION", '"0.62.0"'
buildConfigField "String", "RECOMMENDED_SERVER_VERSION", '"0.63.0"'
applicationIdSuffix ".dev"
}
}
......@@ -58,6 +62,8 @@ dependencies {
implementation libraries.design
implementation libraries.constraintLayout
implementation libraries.cardView
implementation libraries.flexbox
implementation libraries.customTabs
implementation libraries.androidKtx
......@@ -93,14 +99,9 @@ dependencies {
implementation libraries.kotshiApi
implementation libraries.frescoImageViewer
implementation (libraries.androidSvg) {
exclude group: 'org.jetbrains', module: 'annotations-java5'
}
implementation libraries.markwon
implementation (libraries.markwonImageLoader) {
exclude group: 'com.caverock', module: 'androidsvg'
}
implementation libraries.markwonImageLoader
implementation libraries.sheetMenu
......
......@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
......@@ -35,16 +35,30 @@
</intent-filter>
</activity>
<activity
android:name=".server.ui.ChangeServerActivity"
android:theme="@style/AuthenticationTheme" />
<activity
android:name=".main.ui.MainActivity"
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<activity
android:name=".webview.WebViewActivity"
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
android:name=".webview.ui.WebViewActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" />
<activity
android:name=".webview.cas.ui.CasWebViewActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" />
<activity
android:name=".webview.oauth.ui.OauthWebViewActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" />
<activity
android:name=".chatroom.ui.ChatRoomActivity"
android:theme="@style/AppTheme"
......
app/src/main/ic_launcher-web.png

20 KB | W: | H:

app/src/main/ic_launcher-web.png

39.1 KB | W: | H:

app/src/main/ic_launcher-web.png
app/src/main/ic_launcher-web.png
app/src/main/ic_launcher-web.png
app/src/main/ic_launcher-web.png
  • 2-up
  • Swipe
  • Onion skin
......@@ -5,6 +5,7 @@ import android.support.v4.graphics.drawable.DrawableCompat
import android.widget.EditText
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.common.model.UserStatus
object DrawableHelper {
......@@ -78,7 +79,7 @@ object DrawableHelper {
* @param drawables The array of Drawable.
* @see compoundDrawable
*/
fun compoundDrawables(textView: Array<EditText>, drawables: Array<Drawable>) {
fun compoundDrawables(textView: Array<TextView>, drawables: Array<Drawable>) {
if (textView.size != drawables.size) {
return
} else {
......@@ -104,15 +105,15 @@ object DrawableHelper {
* @param context The context.
* @return The user status drawable.
*/
fun getUserStatusDrawable(userStatus: String, context: Context): Drawable {
fun getUserStatusDrawable(userStatus: UserStatus, context: Context): Drawable {
val userStatusDrawable = getDrawableFromId(R.drawable.ic_user_status_black, context).mutate()
wrapDrawable(userStatusDrawable)
when (userStatus) {
// TODO: create a enum or check if it will come from the SDK
"online" -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusOnline)
"busy" -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusBusy)
"away" -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusAway)
"offline" -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusOffline)
is UserStatus.Online -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusOnline)
is UserStatus.Busy -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusBusy)
is UserStatus.Away -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusAway)
is UserStatus.Offline -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusOffline)
else -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusOffline)
}
return userStatusDrawable
}
......
package chat.rocket.android.app
data class User(val id: String,
val name: String,
val username: String,
val status: String,
val avatarUri: String)
\ No newline at end of file
package chat.rocket.android.app.migration
import chat.rocket.android.BuildConfig
import chat.rocket.android.app.migration.model.RealmUser
import io.realm.DynamicRealm
import io.realm.RealmMigration
class RealmMigration : RealmMigration {
override fun migrate(dynamicRealm: DynamicRealm, oldVersion: Long, newVersion: Long) {
var oldVersion = oldVersion
val schema = dynamicRealm.schema
if (oldVersion == 0L) {
// NOOP
oldVersion++
}
if (oldVersion == 1L) {
oldVersion++
}
if (oldVersion == 2L) {
oldVersion++
}
if (oldVersion == 3L) {
oldVersion++
}
if (oldVersion == 4L) {
oldVersion++
}
if (oldVersion == 5L) {
val userSchema = schema.get("RealmUser")
try {
userSchema?.addField(RealmUser.NAME, String::class.java)
} catch (e: IllegalArgumentException) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
// ignore; it makes here if the schema for this model was already update before without migration
}
}
}
// hack around to avoid "new different configuration cannot access the same file" error
override fun hashCode(): Int {
return 37
}
override fun equals(o: Any?): Boolean {
return o is chat.rocket.android.app.migration.RealmMigration
}
// end hack
}
\ No newline at end of file
package chat.rocket.android.app.migration
import io.realm.annotations.RealmModule
@RealmModule(library = true, allClasses = true)
class RocketChatLibraryModule
\ No newline at end of file
package chat.rocket.android.app.migration
import chat.rocket.android.app.migration.model.RealmBasedServerInfo
import io.realm.annotations.RealmModule
@RealmModule(library = true, classes = arrayOf(RealmBasedServerInfo::class))
class RocketChatServerModule
\ No newline at end of file
package chat.rocket.android.app.migration.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class RealmBasedServerInfo : RealmObject() {
@PrimaryKey
@JvmField var hostname: String? = null
@JvmField var name: String? = null
@JvmField var session: String? = null
@JvmField var insecure: Boolean = false
}
\ No newline at end of file
package chat.rocket.android.app.migration.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class RealmEmail : RealmObject() {
@PrimaryKey
@JvmField
var address: String? = null
@JvmField
var verified: Boolean = false
}
\ No newline at end of file
package chat.rocket.android.app.migration.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class RealmPreferences : RealmObject() {
@PrimaryKey
@JvmField
var id: String? = null
@JvmField
var newRoomNotification: String? = null
@JvmField
var newMessageNotification: String? = null
@JvmField
var useEmojis: Boolean = false
@JvmField
var convertAsciiEmoji: Boolean = false
@JvmField
var saveMobileBandwidth: Boolean = false
@JvmField
var collapseMediaByDefault: Boolean = false
@JvmField
var unreadRoomsMode: Boolean = false
@JvmField
var autoImageLoad: Boolean = false
@JvmField
var emailNotificationMode: String? = null
@JvmField
var unreadAlert: Boolean = false
@JvmField
var desktopNotificationDuration: Int = 0
@JvmField
var viewMode: Int = 0
@JvmField
var hideUsernames: Boolean = false
@JvmField
var hideAvatars: Boolean = false
@JvmField
var hideFlexTab: Boolean = false
}
\ No newline at end of file
package chat.rocket.android.app.migration.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class RealmPublicSetting : RealmObject() {
@PrimaryKey
@JvmField
var _id: String? = null
@JvmField
var group: String? = null
@JvmField
var type: String? = null
@JvmField
var value: String? = null
@JvmField
var _updatedAt: Long = 0
@JvmField
var meta: String? = null
}
\ No newline at end of file
package chat.rocket.android.app.migration.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class RealmSession : RealmObject() {
@JvmField
@PrimaryKey
var sessionId: Int = 0 //only 0 is used!
@JvmField
var token: String? = null
@JvmField
var tokenVerified: Boolean = false
@JvmField
var error: String? = null
}
\ No newline at end of file
package chat.rocket.android.app.migration.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class RealmSettings : RealmObject() {
@PrimaryKey
@JvmField
var id: String? = null
@JvmField
var preferences: RealmPreferences? = null
}
\ No newline at end of file
package chat.rocket.android.app.migration.model
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class RealmUser : RealmObject() {
companion object {
const val ID = "_id"
const val NAME = "name"
const val USERNAME = "username"
const val STATUS = "status"
const val UTC_OFFSET = "utcOffset"
const val EMAILS = "emails"
const val SETTINGS = "settings"
const val STATUS_ONLINE = "online"
const val STATUS_BUSY = "busy"
const val STATUS_AWAY = "away"
const val STATUS_OFFLINE = "offline"
}
@PrimaryKey
@JvmField
var _id: String? = null
@JvmField
var name: String? = null
@JvmField
var username: String? = null
@JvmField
var status: String? = null
@JvmField
var utcOffset: Double = 0.toDouble()
@JvmField
var emails: RealmList<RealmEmail>? = null
@JvmField
var settings: RealmSettings? = null
}
\ No newline at end of file
package chat.rocket.android.app.utils
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.support.annotation.ColorInt
import android.support.annotation.Nullable
import android.support.v4.graphics.ColorUtils
import com.facebook.common.internal.ByteStreams
import com.facebook.imageformat.ImageFormat
import com.facebook.imageformat.ImageFormatCheckerUtils
import com.facebook.imagepipeline.common.ImageDecodeOptions
import com.facebook.imagepipeline.decoder.ImageDecoder
import com.facebook.imagepipeline.drawable.DrawableFactory
import com.facebook.imagepipeline.image.CloseableImage
import com.facebook.imagepipeline.image.EncodedImage
import com.facebook.imagepipeline.image.QualityInfo
import java.io.IOException
/**
* Simple decoder that can decode color images that have the following format: <color>#FF5722</color>.
*
* @see {https://github.com/facebook/fresco/blob/master/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/imageformat/color/ColorImageExample.java}
*/
object ColorImage {
// Custom ImageFormat for color images.
private val imageFormatColor = ImageFormat("IMAGE_FORMAT_COLOR", "color")
// XML color tag that our colors must start with.
val colorTag = "<color>"
/**
* Creates a new image format checker for [ColorImage.imageFormatColor].
*
* @return the image format checker.
*/
fun createFormatChecker(): ImageFormat.FormatChecker = ColorFormatChecker()
/**
* Creates a new decoder that can decode [ColorImage.imageFormatColor] images.
*
* @return the decoder.
*/
fun createDecoder(): ImageDecoder = ColorDecoder()
fun createDrawableFactory(): ColorDrawableFactory = ColorDrawableFactory()
/**
* Custom color format checker that verifies that the header of the file corresponds to our [ColorImage.colorTag].
*/
class ColorFormatChecker : ImageFormat.FormatChecker {
private val header = ImageFormatCheckerUtils.asciiBytes(colorTag)
override fun getHeaderSize(): Int {
return header.size
}
@Nullable override fun determineFormat(headerBytes: ByteArray, headerSize: Int): ImageFormat? {
if (headerSize > getHeaderSize()) {
if (ImageFormatCheckerUtils.startsWithPattern(headerBytes, header)) {
return imageFormatColor
}
}
return null
}
}
/**
* Custom closeable color image that holds a single color int value.
*/
class CloseableColorImage(@field:ColorInt @get:ColorInt val color: Int) : CloseableImage() {
private var isClosed = false
override fun close() {
isClosed = true
}
override fun getSizeInBytes(): Int = 0
override fun isClosed(): Boolean = isClosed
override fun getWidth(): Int = 0
override fun getHeight(): Int = 0
}
/**
* Decodes a color XML tag: <color>#rrggbb</color>.
*/
class ColorDecoder : ImageDecoder {
@Nullable override fun decode(encodedImage: EncodedImage, length: Int, qualityInfo: QualityInfo, options: ImageDecodeOptions): CloseableImage? {
try {
// Read the file as a string
val text = String(ByteStreams.toByteArray(encodedImage.inputStream))
// Check if the string matches "<color>#"
if (!text.startsWith(colorTag + "#")) {
return null
}
// Parse the int value between # and <
val startIndex = colorTag.length + 1
val endIndex = text.lastIndexOf('<')
var color = Integer.parseInt(text.substring(startIndex, endIndex), 16)
// Add the alpha component so that we actually see the color
color = ColorUtils.setAlphaComponent(color, 255)
// Return the CloseableImage
return CloseableColorImage(color)
} catch (e: IOException) {
// TODO: are we using the android.util.Log for logging that type of errors? or should we use the SDK logger?
e.printStackTrace()
}
// Return nothing if an error occurred
return null
}
}
/**
* Color drawable factory that is able to render a [CloseableColorImage] by creating a new [ColorDrawable] for the given color.
*/
class ColorDrawableFactory : DrawableFactory {
override fun supportsImageType(image: CloseableImage): Boolean {
// We can only handle CloseableColorImages.
return image is CloseableColorImage
}
@Nullable override fun createDrawable(image: CloseableImage): Drawable? {
// Just return a simple ColorDrawable with the given color value.
return ColorDrawable((image as CloseableColorImage).color)
}
}
}
\ No newline at end of file
package chat.rocket.android.app.utils
import com.facebook.drawee.backends.pipeline.DraweeConfig
import com.facebook.imagepipeline.decoder.ImageDecoderConfig
/**
* Utility class to add custom decoders and drawable factories.
*
* @see {https://github.com/facebook/fresco/blob/master/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/CustomImageFormatConfigurator.java}
*/
object CustomImageFormatConfigurator {
fun createImageDecoderConfig() : ImageDecoderConfig {
return ImageDecoderConfig.newBuilder()
.addDecodingCapability(SvgDecoder.svgFormat, SvgDecoder.SvgFormatChecker(), SvgDecoder.Decoder())
.build()
}
fun addCustomDrawableFactories(draweeConfigBuilder: DraweeConfig.Builder) {
// We always add the color drawable factory so that it can be used for image decoder overrides.
draweeConfigBuilder.addCustomDrawableFactory(ColorImage.createDrawableFactory())
draweeConfigBuilder.addCustomDrawableFactory(SvgDecoder.SvgDrawableFactory())
}
}
\ No newline at end of file
package chat.rocket.android.app.utils
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.graphics.drawable.PictureDrawable
import android.support.annotation.Nullable
import com.caverock.androidsvg.SVG
import com.caverock.androidsvg.SVGParseException
import com.facebook.imageformat.ImageFormat
import com.facebook.imageformat.ImageFormatCheckerUtils
import com.facebook.imagepipeline.common.ImageDecodeOptions
import com.facebook.imagepipeline.decoder.ImageDecoder
import com.facebook.imagepipeline.drawable.DrawableFactory
import com.facebook.imagepipeline.image.CloseableImage
import com.facebook.imagepipeline.image.EncodedImage
import com.facebook.imagepipeline.image.QualityInfo
/**
* SVG example that defines all classes required to decode and render SVG images.
*
* @see {https://github.com/facebook/fresco/blob/master/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/imageformat/svg/SvgDecoderExample.java}
*/
object SvgDecoder {
val svgFormat = ImageFormat("SVG_FORMAT", "svg")
// We do not include the closing ">" since there can be additional information.
private val headerTag = "<svg"
private val possibleHeaderTags = arrayOf(ImageFormatCheckerUtils.asciiBytes("<?xml"))
/**
* Custom SVG format checker that verifies that the header of the file corresponds to our [SvgDecoder.headerTag] or [SvgDecoder.possibleHeaderTags].
*/
class SvgFormatChecker : ImageFormat.FormatChecker {
private val header = ImageFormatCheckerUtils.asciiBytes(headerTag)
override fun getHeaderSize(): Int {
return header.size
}
@Nullable override fun determineFormat(headerBytes: ByteArray, headerSize: Int): ImageFormat? {
if (headerSize > getHeaderSize()) {
if (ImageFormatCheckerUtils.startsWithPattern(headerBytes, header)) {
return svgFormat
}
if (possibleHeaderTags.any { ImageFormatCheckerUtils.startsWithPattern(headerBytes, it) && ImageFormatCheckerUtils.indexOfPattern(headerBytes, headerBytes.size, header, header.size) > -1 }) {
return svgFormat
}
}
return null
}
}
/**
* Custom closeable SVG image that holds a single SVG.
*/
class CloseableSvgImage(val svg: SVG) : CloseableImage() {
private var isClose = false
override fun close() {
isClose = true
}
override fun getSizeInBytes(): Int = 0
override fun isClosed(): Boolean = isClose
override fun getWidth(): Int = 0
override fun getHeight(): Int = 0
}
/**
* Decodes a [SvgDecoder.svgFormat] image.
*/
class Decoder : ImageDecoder {
@Nullable override fun decode(encodedImage: EncodedImage, length: Int, qualityInfo: QualityInfo, options: ImageDecodeOptions): CloseableImage? {
try {
val svg = SVG.getFromInputStream(encodedImage.inputStream)
return CloseableSvgImage(svg)
} catch (e: SVGParseException) {
// TODO: are we using the android.util.Log for logging that type of errors? or should we use the SDK logger?
e.printStackTrace()
}
// Return nothing if an error occurred
return null
}
}
/**
* SVG drawable factory that creates [PictureDrawable]s for SVG images.
*/
class SvgDrawableFactory : DrawableFactory {
override fun supportsImageType(image: CloseableImage): Boolean {
return image is CloseableSvgImage
}
@Nullable override fun createDrawable(image: CloseableImage): Drawable? {
return SvgPictureDrawable((image as CloseableSvgImage).svg)
}
}
class SvgPictureDrawable(private val svg: SVG) : PictureDrawable(null) {
override fun onBoundsChange(bounds: Rect) {
super.onBoundsChange(bounds)
picture = svg.renderToPicture(bounds.width(), bounds.height())
}
}
}
\ No newline at end of file
package chat.rocket.android.authentication.di
import android.content.Context
import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.dagger.scope.PerActivity
......@@ -12,5 +11,5 @@ class AuthenticationModule {
@Provides
@PerActivity
fun provideAuthenticationNavigator(activity: AuthenticationActivity, context: Context) = AuthenticationNavigator(activity, context)
fun provideAuthenticationNavigator(activity: AuthenticationActivity) = AuthenticationNavigator(activity)
}
\ No newline at end of file
package chat.rocket.android.authentication.domain.model
import chat.rocket.common.model.Token
import se.ansman.kotshi.JsonSerializable
@JsonSerializable
data class TokenModel(val userId: String, val authToken: String)
\ No newline at end of file
data class TokenModel(val userId: String, val authToken: String)
fun TokenModel.toToken() = Token(userId, authToken)
\ No newline at end of file
package chat.rocket.android.authentication.infraestructure
import chat.rocket.common.model.Token
import chat.rocket.core.TokenRepository
class MemoryTokenRepository : TokenRepository {
var savedToken: Token? = null
override fun get(): Token? {
return savedToken
}
override fun save(token: Token) {
savedToken = token
}
}
\ No newline at end of file
package chat.rocket.android.authentication.infraestructure
import android.content.SharedPreferences
import androidx.content.edit
import chat.rocket.android.authentication.domain.model.TokenModel
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.common.model.Token
import com.squareup.moshi.Moshi
import timber.log.Timber
class SharedPreferencesTokenRepository(private val prefs: SharedPreferences, moshi: Moshi) : TokenRepository {
private var servers = prefs.getStringSet(KEY_SERVERS, emptySet()).toMutableSet()
private var currentUrl: String? = null
private var currentToken: Token? = null
private val adapter = moshi.adapter<TokenModel>(TokenModel::class.java)
override fun get(url: String): Token? {
if (currentToken != null && url == currentUrl) {
return currentToken
}
try {
prefs.getString(tokenKey(url), null)?.let { tokenStr ->
val model = adapter.fromJson(tokenStr)
model?.let {
val token = Token(model.userId, model.authToken)
currentToken = token
currentUrl = url
}
}
} catch (ex: Exception) {
Timber.d(ex, "Error parsing token for ${tokenKey(url)}")
ex.printStackTrace()
}
return currentToken
}
override fun save(url: String, token: Token) {
try {
val model = TokenModel(token.userId, token.authToken)
val str = adapter.toJson(model)
servers.add(url)
prefs.edit {
putString(tokenKey(url), str)
putStringSet(KEY_SERVERS, servers)
}
currentToken = token
currentUrl = url
} catch (ex: Exception) {
Timber.d(ex, "Error saving token for ${tokenKey(url)}")
ex.printStackTrace()
}
}
override fun remove(url: String) {
servers.remove(url)
prefs.edit {
remove(url)
putStringSet(KEY_SERVERS, servers)
}
}
override fun clear() {
servers.forEach { server ->
prefs.edit { remove(server) }
}
servers.clear()
prefs.edit {
remove(KEY_SERVERS)
}
}
private fun tokenKey(url: String) = "$KEY_TOKEN$url"
}
private const val KEY_TOKEN = "KEY_TOKEN_"
private const val KEY_SERVERS = "KEY_SERVERS"
\ No newline at end of file
package chat.rocket.android.authentication.login.presentation
import chat.rocket.android.authentication.server.presentation.VersionCheckView
import chat.rocket.android.core.behaviours.InternetView
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
interface LoginView : LoadingView, MessageView, InternetView {
interface LoginView : LoadingView, MessageView, InternetView, VersionCheckView {
/**
* Shows the oauth view if the server settings allow the login via social accounts.
* Shows the form view (i.e the username/email and password fields) if it is enabled by the server settings.
*
* REMARK: we must show at maximum *three* social accounts views ([enableLoginByFacebook], [enableLoginByGithub], [enableLoginByGoogle],
* REMARK: We must set up the login button listener [setupLoginButtonListener].
* Remember to enable [enableUserInput] or disable [disableUserInput] the view interaction for the user when submitting the form.
*/
fun showFormView()
/**
* Hides the form view.
*/
fun hideFormView()
/**
* Setups the login button when tapped.
*/
fun setupLoginButtonListener()
/**
* Enables the view interactions for the user.
*/
fun enableUserInput()
/**
* Disables the view interactions for the user.
*/
fun disableUserInput()
/**
* Shows the CAS button if the sign in/sign out via CAS protocol is enabled by the server settings.
*
* REMARK: We must set up the CAS button listener before showing it [setupCasButtonListener].
*/
fun showCasButton()
/**
* Hides the CAS button.
*/
fun hideCasButton()
/**
* Setups the CAS button when tapped.
*
* @param casUrl The CAS URL to authenticate with.
* @param casToken The requested token to be sent to the CAS server.
*/
fun setupCasButtonListener(casUrl: String, casToken: String)
/**
* Shows the sign up view if the new users registration is enabled by the server settings.
*
* REMARK: We must set up the sign up view listener [setupSignUpView].
*/
fun showSignUpView()
/**
* Setups the sign up view when tapped.
*/
fun setupSignUpView()
/**
* Hides the sign up view.
*/
fun hideSignUpView()
/**
* Enables and shows the oauth view if there is login via social accounts enabled by the server settings.
*
* 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)
fun enableOauthView()
/**
* Setups the FloatingActionButton to show more social accounts views (expanding the oauth view interface to show the remaining view(s)).
* Disables and hides the Oauth view if there is not login via social accounts enabled by the server settings.
*/
fun setupFabListener()
fun disableOauthView()
/**
* Shows the login button.
*/
fun showLoginButton()
/**
* Hides the login button.
*/
fun hideLoginButton()
/**
* Shows the login by Facebook view.
* Shows the "login by Facebook view if it is enable by the server settings.
*/
fun enableLoginByFacebook()
/**
* Shows the login by Github view.
* Shows the "login by Github" view if it is enable by the server settings.
*
* REMARK: We must set up the Github button listener before enabling it [setupGithubButtonListener].
*/
fun enableLoginByGithub()
/**
* Shows the login by Google view.
* Setups the Github button when tapped.
*
* @param githubUrl The Github OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun setupGithubButtonListener(githubUrl: String, state: String)
/**
* Shows the "login by Google" view if it is enable by the server settings.
*
* REMARK: We must set up the Google button listener before enabling it [setupGoogleButtonListener].
*/
fun enableLoginByGoogle()
/**
* Shows the login by Linkedin view.
* Setups the Google button when tapped.
*
* @param googleUrl The Google OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun setupGoogleButtonListener(googleUrl: String, state: String)
/**
* Shows the "login by Linkedin" view if it is enable by the server settings.
*
* REMARK: We must set up the Linkedin button listener before enabling it [setupLinkedinButtonListener].
*/
fun enableLoginByLinkedin()
/**
* Shows the login by Meteor view.
* Setups the Linkedin button when tapped.
*
* @param linkedinUrl The Linkedin OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun setupLinkedinButtonListener(linkedinUrl: String, state: String)
/**
* Shows the "login by Meteor" view if it is enable by the server settings.
*/
fun enableLoginByMeteor()
/**
* Shows the login by Twitter view.
* Shows the "login by Twitter" view if it is enable by the server settings.
*/
fun enableLoginByTwitter()
/**
* Shows the login by Gitlab view.
* Shows the "login by Gitlab" view if it is enable by the server settings.
*
* REMARK: We must set up the Gitlab button listener before enabling it [setupGitlabButtonListener].
*/
fun enableLoginByGitlab()
/**
* Shows the sign up view if the server settings allow the new users registration.
* Setups the Gitlab button when tapped.
*
* @param value True to show the sign up view, false otherwise.
* @param gitlabUrl The Gitlab OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun showSignUpView(value: Boolean)
fun setupGitlabButtonListener(gitlabUrl: String, state: String)
/**
* Setups the FloatingActionButton to show more social accounts views (expanding the oauth view interface to show the remaining view(s)).
*/
fun setupFabListener()
fun setupGlobalListener()
/**
* Alerts the user about a wrong inputted username or email.
......@@ -73,4 +187,9 @@ interface LoginView : LoadingView, MessageView, InternetView {
* Alerts the user about a wrong inputted password.
*/
fun alertWrongPassword()
/**
* Alerts the user about the need of creating an username using the web app when creating an user through OAuth.
*/
fun alertRequiresUsername()
}
\ No newline at end of file
package chat.rocket.android.authentication.presentation
import android.content.Context
import android.content.Intent
import chat.rocket.android.R
import chat.rocket.android.authentication.login.ui.LoginFragment
import chat.rocket.android.authentication.signup.ui.SignupFragment
import chat.rocket.android.authentication.twofactor.ui.TwoFAFragment
import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.authentication.ui.newServerIntent
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.server.ui.changeServerIntent
import chat.rocket.android.util.extensions.addFragmentBackStack
import chat.rocket.android.webview.webViewIntent
import chat.rocket.android.webview.ui.webViewIntent
class AuthenticationNavigator(internal val activity: AuthenticationActivity, internal val context: Context) {
class AuthenticationNavigator(internal val activity: AuthenticationActivity) {
fun toLogin() {
activity.addFragmentBackStack("LoginFragment", R.id.fragment_container) {
......@@ -32,19 +33,22 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity, int
}
fun toWebPage(url: String) {
activity.startActivity(context.webViewIntent(url))
activity.startActivity(activity.webViewIntent(url))
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.startActivity(Intent(activity, MainActivity::class.java))
activity.finish()
}
fun toChatList(serverUrl: String) {
activity.startActivity(activity.changeServerIntent(serverUrl))
activity.finish()
}
fun toServerScreen() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
activity.startActivity(activity.newServerIntent())
activity.finish()
}
}
}
\ No newline at end of file
package chat.rocket.android.authentication.presentation
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetAccountInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.MultiServerTokenRepository
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.common.model.Token
import chat.rocket.core.TokenRepository
import chat.rocket.android.server.domain.TokenRepository
import javax.inject.Inject
class AuthenticationPresenter @Inject constructor(private val navigator: AuthenticationNavigator,
private val getCurrentServerInteractor: GetCurrentServerInteractor,
private val multiServerRepository: MultiServerTokenRepository,
private val settingsRepository: SettingsRepository,
private val tokenRepository: TokenRepository) {
fun loadCredentials(callback: (authenticated: Boolean) -> Unit) {
class AuthenticationPresenter @Inject constructor(
private val navigator: AuthenticationNavigator,
private val getCurrentServerInteractor: GetCurrentServerInteractor,
private val getAccountInteractor: GetAccountInteractor,
private val settingsRepository: SettingsRepository,
private val localRepository: LocalRepository,
private val tokenRepository: TokenRepository
) {
suspend fun loadCredentials(newServer: Boolean, callback: (authenticated: Boolean) -> Unit) {
val currentServer = getCurrentServerInteractor.get()
val serverToken = currentServer?.let { multiServerRepository.get(currentServer) }
val serverToken = currentServer?.let { tokenRepository.get(currentServer) }
val settings = currentServer?.let { settingsRepository.get(currentServer) }
val account = currentServer?.let { getAccountInteractor.get(currentServer) }
account?.let {
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, account.userName)
}
if (currentServer == null || serverToken == null || settings == null) {
if (newServer || currentServer == null || serverToken == null || settings == null) {
callback(false)
} else {
tokenRepository.save(Token(serverToken.userId, serverToken.authToken))
callback(true)
navigator.toChatList()
}
......
......@@ -4,6 +4,7 @@ import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.server.domain.GetAccountsInteractor
import chat.rocket.android.server.domain.RefreshSettingsInteractor
import chat.rocket.android.server.domain.SaveCurrentServerInteractor
import chat.rocket.android.util.extensions.launchUI
......@@ -14,19 +15,25 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator,
private val serverInteractor: SaveCurrentServerInteractor,
private val refreshSettingsInteractor: RefreshSettingsInteractor) {
private val refreshSettingsInteractor: RefreshSettingsInteractor,
private val getAccountsInteractor: GetAccountsInteractor) {
fun connect(server: String) {
if (!UrlHelper.isValidUrl(server)) {
view.showInvalidServerUrl()
view.showInvalidServerUrlMessage()
} else {
launchUI(strategy) {
// Check if we already have an account for this server...
val account = getAccountsInteractor.get().firstOrNull { it.serverUrl == server }
if (account != null) {
navigator.toChatList(server)
return@launchUI
}
if (NetworkHelper.hasInternetAccess()) {
view.showLoading()
try {
refreshSettingsInteractor.refresh(server)
serverInteractor.save(server)
navigator.toLogin()
} catch (ex: Exception) {
ex.message?.let {
......
......@@ -7,7 +7,7 @@ import chat.rocket.android.core.behaviours.MessageView
interface ServerView : LoadingView, MessageView, InternetView {
/**
* Notifies the user about an invalid inputted server URL.
* Shows an invalid server URL message.
*/
fun showInvalidServerUrl()
fun showInvalidServerUrlMessage()
}
\ No newline at end of file
package chat.rocket.android.authentication.server.presentation
interface VersionCheckView {
/**
* Alerts the user about the server version not meeting the recommended server version.
*/
fun alertNotRecommendedVersion()
/**
* Block user to proceed and alert him due to server having an unsupported server version.
*/
fun blockAndAlertNotRequiredVersion()
}
\ No newline at end of file
......@@ -30,7 +30,8 @@ class ServerFragment : Fragment(), ServerView {
AndroidSupportInjection.inject(this)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_authentication_server)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
container?.inflate(R.layout.fragment_authentication_server)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
......@@ -43,7 +44,7 @@ class ServerFragment : Fragment(), ServerView {
relative_layout.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
}
override fun showInvalidServerUrl() = showMessage(getString(R.string.msg_invalid_server_url))
override fun showInvalidServerUrlMessage() = showMessage(getString(R.string.msg_invalid_server_url))
override fun showLoading() {
enableUserInput(false)
......@@ -55,13 +56,21 @@ class ServerFragment : Fragment(), ServerView {
enableUserInput(true)
}
override fun showMessage(resId: Int) = showToast(resId)
override fun showMessage(resId: Int){
showToast(resId)
}
override fun showMessage(message: String) = showToast(message)
override fun showMessage(message: String) {
showToast(message)
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showGenericErrorMessage() {
showMessage(getString(R.string.msg_generic_error))
}
override fun showNoInternetConnection() = showMessage(getString(R.string.msg_no_internet_connection))
override fun showNoInternetConnection() {
showMessage(getString(R.string.msg_no_internet_connection))
}
private fun enableUserInput(value: Boolean) {
button_connect.isEnabled = value
......
......@@ -5,9 +5,12 @@ import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.main.viewmodel.NavHeaderViewModel
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.extensions.registerPushToken
import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
......@@ -15,6 +18,7 @@ import chat.rocket.core.internal.rest.login
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.registerPushToken
import chat.rocket.core.internal.rest.signup
import chat.rocket.core.model.Myself
import javax.inject.Inject
class SignupPresenter @Inject constructor(private val view: SignupView,
......@@ -22,8 +26,13 @@ class SignupPresenter @Inject constructor(private val view: SignupView,
private val navigator: AuthenticationNavigator,
private val localRepository: LocalRepository,
private val serverInteractor: GetCurrentServerInteractor,
private val factory: RocketChatClientFactory) {
private val client: RocketChatClient = factory.create(serverInteractor.get()!!)
private val factory: RocketChatClientFactory,
private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
settingsInteractor: GetSettingsInteractor) {
private val currentServer = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(currentServer)
private var settings: PublicSettings = settingsInteractor.get(serverInteractor.get()!!)
fun signup(name: String, username: String, password: String, email: String) {
val server = serverInteractor.get()
......@@ -55,7 +64,8 @@ class SignupPresenter @Inject constructor(private val view: SignupView,
// TODO This function returns a user token so should we save it?
client.login(username, password)
val me = client.me()
localRepository.save(LocalRepository.USERNAME_KEY, me.username)
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, me.username)
saveAccount(me)
registerPushToken()
navigator.toChatList()
} catch (exception: RocketChatException) {
......@@ -90,8 +100,21 @@ class SignupPresenter @Inject constructor(private val view: SignupView,
private suspend fun registerPushToken() {
localRepository.get(LocalRepository.KEY_PUSH_TOKEN)?.let {
client.registerPushToken(it)
client.registerPushToken(it, getAccountsInteractor.get(), factory)
}
// TODO: Schedule push token registering when it comes up null
// TODO: When the push token is null, at some point we should receive it with
// onTokenRefresh() on FirebaseTokenService, we need to confirm it.
}
private suspend fun saveAccount(me: Myself) {
val icon = settings.favicon()?.let {
UrlHelper.getServerLogoUrl(currentServer, it)
}
val logo = settings.wideTile()?.let {
UrlHelper.getServerLogoUrl(currentServer, it)
}
val thumb = UrlHelper.getAvatarUrl(currentServer, me.username!!)
val account = Account(currentServer, icon, logo, me.username!!, thumb)
saveAccountInteractor.save(account)
}
}
\ No newline at end of file
package chat.rocket.android.authentication.signup.ui
import DrawableHelper
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.support.v4.app.Fragment
......@@ -11,25 +10,24 @@ import android.widget.Toast
import chat.rocket.android.R
import chat.rocket.android.authentication.signup.presentation.SignupPresenter
import chat.rocket.android.authentication.signup.presentation.SignupView
import chat.rocket.android.helper.AnimationHelper
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.TextHelper
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.*
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_sign_up.*
import javax.inject.Inject
class SignupFragment : Fragment(), SignupView {
@Inject lateinit var presenter: SignupPresenter
@Inject lateinit var appContext: Context
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
if (KeyboardHelper.isSoftKeyboardShown(constraint_layout.rootView)) {
text_new_user_agreement.setVisible(false)
if (KeyboardHelper.isSoftKeyboardShown(relative_layout.rootView)) {
bottom_container.setVisible(false)
} else {
text_new_user_agreement.setVisible(true)
bottom_container.apply {
postDelayed({
setVisible(true)
}, 3)
}
}
}
......@@ -47,47 +45,45 @@ class SignupFragment : Fragment(), SignupView {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activity?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
tintEditTextDrawableStart()
}
constraint_layout.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
relative_layout.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
setUpNewUserAgreementListener()
button_sign_up.setOnClickListener {
presenter.signup(text_name.textContent, text_username.textContent, text_password.textContent, text_email.textContent)
presenter.signup(text_username.textContent, text_username.textContent, text_password.textContent, text_email.textContent)
}
}
override fun onDestroyView() {
relative_layout.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
super.onDestroyView()
constraint_layout.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
}
override fun alertBlankName() {
AnimationHelper.vibrateSmartPhone(appContext)
AnimationHelper.shakeView(text_name)
vibrateSmartPhone()
text_name.shake()
text_name.requestFocus()
}
override fun alertBlankUsername() {
AnimationHelper.vibrateSmartPhone(appContext)
AnimationHelper.shakeView(text_username)
vibrateSmartPhone()
text_username.shake()
text_username.requestFocus()
}
override fun alertEmptyPassword() {
AnimationHelper.vibrateSmartPhone(appContext)
AnimationHelper.shakeView(text_password)
vibrateSmartPhone()
text_password.shake()
text_password.requestFocus()
}
override fun alertBlankEmail() {
AnimationHelper.vibrateSmartPhone(appContext)
AnimationHelper.shakeView(text_email)
vibrateSmartPhone()
text_email.shake()
text_email.requestFocus()
}
......@@ -101,9 +97,13 @@ class SignupFragment : Fragment(), SignupView {
enableUserInput(true)
}
override fun showMessage(resId: Int) = showToast(resId)
override fun showMessage(resId: Int) {
showToast(resId)
}
override fun showMessage(message: String) = showToast(message)
override fun showMessage(message: String) {
showToast(message)
}
override fun showGenericErrorMessage() {
showMessage(getString(R.string.msg_generic_error))
......@@ -114,15 +114,17 @@ class SignupFragment : Fragment(), SignupView {
}
private fun tintEditTextDrawableStart() {
val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_person_black_24dp, appContext)
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)
activity?.apply {
val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_person_black_24dp, this)
val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, this)
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 setUpNewUserAgreementListener() {
......@@ -149,7 +151,7 @@ class SignupFragment : Fragment(), SignupView {
private fun enableUserInput(value: Boolean) {
button_sign_up.isEnabled = value
text_name.isEnabled = value
text_username.isEnabled = value
text_username.isEnabled = value
text_password.isEnabled = value
text_email.isEnabled = value
......
package chat.rocket.android.authentication.twofactor.presentation
import chat.rocket.android.authentication.domain.model.TokenModel
import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.MultiServerTokenRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.extensions.registerPushToken
import chat.rocket.common.RocketChatAuthException
import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.login
import chat.rocket.core.internal.rest.registerPushToken
import chat.rocket.core.internal.rest.me
import chat.rocket.core.model.Myself
import javax.inject.Inject
class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator,
private val multiServerRepository: MultiServerTokenRepository,
private val tokenRepository: TokenRepository,
private val localRepository: LocalRepository,
private val serverInteractor: GetCurrentServerInteractor,
private val factory: RocketChatClientFactory) {
private val client: RocketChatClient = factory.create(serverInteractor.get()!!)
private val factory: RocketChatClientFactory,
private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
settingsInteractor: GetSettingsInteractor) {
private val currentServer = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(currentServer)
private var settings: PublicSettings = settingsInteractor.get(serverInteractor.get()!!)
// TODO: If the usernameOrEmail and password was informed by the user on the previous screen, then we should pass only the pin, like this: fun authenticate(pin: EditText)
fun authenticate(usernameOrEmail: String, password: String, twoFactorAuthenticationCode: String) {
......@@ -45,10 +52,9 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
// The token is saved via the client TokenProvider
val token =
client.login(usernameOrEmail, password, twoFactorAuthenticationCode)
multiServerRepository.save(
server,
TokenModel(token.userId, token.authToken)
)
val me = client.me()
saveAccount(me)
tokenRepository.save(server, token)
registerPushToken()
navigator.toChatList()
} catch (exception: RocketChatException) {
......@@ -76,8 +82,21 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
private suspend fun registerPushToken() {
localRepository.get(LocalRepository.KEY_PUSH_TOKEN)?.let {
client.registerPushToken(it)
client.registerPushToken(it, getAccountsInteractor.get(), factory)
}
// TODO: Schedule push token registering when it comes up null
// TODO: When the push token is null, at some point we should receive it with
// onTokenRefresh() on FirebaseTokenService, we need to confirm it.
}
private suspend fun saveAccount(me: Myself) {
val icon = settings.favicon()?.let {
UrlHelper.getServerLogoUrl(currentServer, it)
}
val logo = settings.wideTile()?.let {
UrlHelper.getServerLogoUrl(currentServer, it)
}
val thumb = UrlHelper.getAvatarUrl(currentServer, me.username!!)
val account = Account(currentServer, icon, logo, me.username!!, thumb)
saveAccountInteractor.save(account)
}
}
\ No newline at end of file
......@@ -12,10 +12,7 @@ import android.view.inputmethod.InputMethodManager
import chat.rocket.android.R
import chat.rocket.android.authentication.twofactor.presentation.TwoFAPresenter
import chat.rocket.android.authentication.twofactor.presentation.TwoFAView
import chat.rocket.android.helper.AnimationHelper
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.*
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_two_fa.*
import javax.inject.Inject
......@@ -65,13 +62,13 @@ class TwoFAFragment : Fragment(), TwoFAView {
}
override fun alertBlankTwoFactorAuthenticationCode() {
activity?.let {
AnimationHelper.vibrateSmartPhone(it)
AnimationHelper.shakeView(text_two_factor_auth)
}
vibrateSmartPhone()
text_two_factor_auth.shake()
}
override fun alertInvalidTwoFactorAuthenticationCode() = showMessage(getString(R.string.msg_invalid_2fa_code))
override fun alertInvalidTwoFactorAuthenticationCode() {
showMessage(getString(R.string.msg_invalid_2fa_code))
}
override fun showLoading() {
enableUserInput(false)
......@@ -83,9 +80,13 @@ class TwoFAFragment : Fragment(), TwoFAView {
enableUserInput(true)
}
override fun showMessage(resId: Int) = showToast(resId)
override fun showMessage(resId: Int) {
showToast(resId)
}
override fun showMessage(message: String) = showToast(message)
override fun showMessage(message: String) {
showToast(message)
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
......
package chat.rocket.android.authentication.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
......@@ -7,40 +9,58 @@ import chat.rocket.android.R
import chat.rocket.android.authentication.presentation.AuthenticationPresenter
import chat.rocket.android.authentication.server.ui.ServerFragment
import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.util.extensions.launchUI
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
import javax.inject.Inject
class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
@Inject lateinit var presenter: AuthenticationPresenter
val job = Job()
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
presenter.loadCredentials { authenticated ->
if (authenticated) {
// just call onCreate, and the presenter will call the navigator...
super.onCreate(savedInstanceState)
} else {
showServerInput(savedInstanceState)
setContentView(R.layout.activity_authentication)
setTheme(R.style.AuthenticationTheme)
super.onCreate(savedInstanceState)
launch(UI + job) {
val newServer = intent.getBooleanExtra(INTENT_ADD_NEW_SERVER, false)
presenter.loadCredentials(newServer) { authenticated ->
if (!authenticated) {
showServerInput(savedInstanceState)
}
}
}
}
override fun onDestroy() {
job.cancel()
super.onDestroy()
}
override fun supportFragmentInjector(): AndroidInjector<Fragment> {
return fragmentDispatchingAndroidInjector
}
fun showServerInput(savedInstanceState: Bundle?) {
setContentView(R.layout.activity_authentication)
setTheme(R.style.AuthenticationTheme)
super.onCreate(savedInstanceState)
addFragment("ServerFragment", R.id.fragment_container) {
ServerFragment.newInstance()
}
}
}
const val INTENT_ADD_NEW_SERVER = "INTENT_ADD_NEW_SERVER"
fun Context.newServerIntent(): Intent {
return Intent(this, AuthenticationActivity::class.java).apply {
putExtra(INTENT_ADD_NEW_SERVER, true)
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
}
\ No newline at end of file
......@@ -4,10 +4,13 @@ import android.view.View
import chat.rocket.android.chatroom.viewmodel.AudioAttachmentViewModel
import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.message_attachment.view.*
class AudioAttachmentViewHolder(itemView: View, listener: ActionsListener)
: BaseViewHolder<AudioAttachmentViewModel>(itemView, listener) {
class AudioAttachmentViewHolder(itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<AudioAttachmentViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
......
package chat.rocket.android.chatroom.adapter
import android.support.annotation.IntDef
const val PEOPLE = 0
const val ROOMS = 1
@Retention(AnnotationRetention.SOURCE)
@IntDef(PEOPLE, ROOMS)
annotation class AutoCompleteType
......@@ -7,16 +7,22 @@ import chat.rocket.android.R
import chat.rocket.android.chatroom.ui.bottomsheet.BottomSheetMenu
import chat.rocket.android.chatroom.ui.bottomsheet.adapter.ActionListAdapter
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.widget.emoji.Emoji
import chat.rocket.android.widget.emoji.EmojiReactionListener
import chat.rocket.core.model.Message
import chat.rocket.core.model.isSystemMessage
import com.google.android.flexbox.FlexDirection
import com.google.android.flexbox.FlexboxLayoutManager
import ru.whalemare.sheetmenu.extension.inflate
import ru.whalemare.sheetmenu.extension.toList
abstract class BaseViewHolder<T : BaseViewModel<*>>(
itemView: View,
private val listener: ActionsListener
itemView: View,
private val listener: ActionsListener,
var reactionListener: EmojiReactionListener? = null
) : RecyclerView.ViewHolder(itemView),
MenuItem.OnMenuItemClickListener {
MenuItem.OnMenuItemClickListener {
var data: T? = null
init {
......@@ -26,6 +32,39 @@ abstract class BaseViewHolder<T : BaseViewModel<*>>(
fun bind(data: T) {
this.data = data
bindViews(data)
bindReactions()
}
private fun bindReactions() {
data?.let {
val recyclerView = itemView.findViewById(R.id.recycler_view_reactions) as RecyclerView
val adapter: MessageReactionsAdapter
if (recyclerView.adapter == null) {
adapter = MessageReactionsAdapter()
} else {
adapter = recyclerView.adapter as MessageReactionsAdapter
adapter.clear()
}
if (it.nextDownStreamMessage == null) {
adapter.listener = object : EmojiReactionListener {
override fun onReactionTouched(messageId: String, emojiShortname: String) {
reactionListener?.onReactionTouched(messageId, emojiShortname)
}
override fun onReactionAdded(messageId: String, emoji: Emoji) {
if (!adapter.contains(emoji.shortname)) {
reactionListener?.onReactionAdded(messageId, emoji)
}
}
}
val context = itemView.context
val manager = FlexboxLayoutManager(context, FlexDirection.ROW)
recyclerView.layoutManager = manager
recyclerView.adapter = adapter
adapter.addReactions(it.reactions.filterNot { it.unicode.startsWith(":") })
}
}
}
abstract fun bindViews(data: T)
......
......@@ -7,15 +7,18 @@ import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.viewmodel.*
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.widget.emoji.EmojiReactionListener
import chat.rocket.core.model.Message
import chat.rocket.core.model.isSystemMessage
import timber.log.Timber
import java.security.InvalidParameterException
class ChatRoomAdapter(
private val roomType: String,
private val roomName: String,
private val presenter: ChatRoomPresenter?,
private val enableActions: Boolean = true
private val roomType: String,
private val roomName: String,
private val presenter: ChatRoomPresenter?,
private val enableActions: Boolean = true,
private val reactionListener: EmojiReactionListener? = null
) : RecyclerView.Adapter<BaseViewHolder<*>>() {
private val dataSet = ArrayList<BaseViewModel<*>>()
......@@ -25,26 +28,30 @@ class ChatRoomAdapter(
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<*> {
return when(viewType.toViewType()) {
return when (viewType.toViewType()) {
BaseViewModel.ViewType.MESSAGE -> {
val view = parent.inflate(R.layout.item_message)
MessageViewHolder(view, actionsListener)
MessageViewHolder(view, actionsListener, reactionListener)
}
BaseViewModel.ViewType.IMAGE_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment)
ImageAttachmentViewHolder(view, actionsListener)
ImageAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseViewModel.ViewType.AUDIO_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment)
AudioAttachmentViewHolder(view, actionsListener)
AudioAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseViewModel.ViewType.VIDEO_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment)
VideoAttachmentViewHolder(view, actionsListener)
VideoAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseViewModel.ViewType.URL_PREVIEW -> {
val view = parent.inflate(R.layout.message_url_preview)
UrlPreviewViewHolder(view, actionsListener)
UrlPreviewViewHolder(view, actionsListener, reactionListener)
}
BaseViewModel.ViewType.MESSAGE_ATTACHMENT -> {
val view = parent.inflate(R.layout.item_message_attachment)
MessageAttachmentViewHolder(view, actionsListener, reactionListener)
}
else -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
......@@ -61,12 +68,30 @@ class ChatRoomAdapter(
}
override fun onBindViewHolder(holder: BaseViewHolder<*>, position: Int) {
if (holder !is MessageViewHolder) {
if (position + 1 < itemCount) {
val messageAbove = dataSet[position + 1]
if (messageAbove.messageId == dataSet[position].messageId) {
messageAbove.nextDownStreamMessage = dataSet[position]
}
}
} else {
if (position == 0) {
dataSet[0].nextDownStreamMessage = null
} else if (position - 1 > 0) {
if (dataSet[position - 1].messageId != dataSet[position].messageId) {
dataSet[position].nextDownStreamMessage = null
}
}
}
when (holder) {
is MessageViewHolder -> holder.bind(dataSet[position] as MessageViewModel)
is ImageAttachmentViewHolder -> holder.bind(dataSet[position] as ImageAttachmentViewModel)
is AudioAttachmentViewHolder -> holder.bind(dataSet[position] as AudioAttachmentViewModel)
is VideoAttachmentViewHolder -> holder.bind(dataSet[position] as VideoAttachmentViewModel)
is UrlPreviewViewHolder -> holder.bind(dataSet[position] as UrlPreviewViewModel)
is MessageAttachmentViewHolder -> holder.bind(dataSet[position] as MessageAttachmentViewModel)
}
}
......@@ -86,16 +111,34 @@ class ChatRoomAdapter(
}
fun prependData(dataSet: List<BaseViewModel<*>>) {
this.dataSet.addAll(0, dataSet)
notifyItemRangeInserted(0, dataSet.size)
val item = dataSet.firstOrNull { newItem ->
this.dataSet.indexOfFirst { it.messageId == newItem.messageId && it.viewType == newItem.viewType } > -1
}
if (item == null) {
this.dataSet.addAll(0, dataSet)
notifyItemRangeInserted(0, dataSet.size)
}
}
fun updateItem(message: BaseViewModel<*>) {
val index = dataSet.indexOfLast { it.messageId == message.messageId }
var index = dataSet.indexOfLast { it.messageId == message.messageId }
val indexOfNext = dataSet.indexOfFirst { it.messageId == message.messageId }
Timber.d("index: $index")
if (index > -1) {
dataSet[index] = message
notifyItemChanged(index)
dataSet.forEachIndexed { index, viewModel ->
if (viewModel.messageId == message.messageId) {
if (viewModel.nextDownStreamMessage == null) {
viewModel.reactions = message.reactions
}
notifyItemChanged(index)
}
}
// Delete message only if current is a system message update, i.e.: Message Removed
if (message.message.isSystemMessage() && indexOfNext > -1 && indexOfNext != index) {
dataSet.removeAt(indexOfNext)
notifyItemRemoved(indexOfNext)
}
}
}
......@@ -131,6 +174,7 @@ class ChatRoomAdapter(
}
}
}
R.id.action_menu_msg_react -> presenter?.showReactions(id)
else -> TODO("Not implemented")
}
}
......
package chat.rocket.android.chatroom.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.CommandSuggestionsAdapter.CommandSuggestionsViewHolder
import chat.rocket.android.chatroom.viewmodel.suggestion.CommandSuggestionViewModel
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.ui.BaseSuggestionViewHolder
import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter
class CommandSuggestionsAdapter : SuggestionsAdapter<CommandSuggestionsViewHolder>(token = "/",
constraint = CONSTRAINT_BOUND_TO_START, threshold = RESULT_COUNT_UNLIMITED) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommandSuggestionsViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.suggestion_command_item, parent,
false)
return CommandSuggestionsViewHolder(view)
}
class CommandSuggestionsViewHolder(view: View) : BaseSuggestionViewHolder(view) {
override fun bind(item: SuggestionModel, itemClickListener: SuggestionsAdapter.ItemClickListener?) {
item as CommandSuggestionViewModel
with(itemView) {
val nameTextView = itemView.findViewById<TextView>(R.id.text_command_name)
val descriptionTextView = itemView.findViewById<TextView>(R.id.text_command_description)
nameTextView.text = "/${item.text}"
val res = context.resources
val id = res.getIdentifier(item.description, "string", context.packageName)
val description = if (id > 0) res.getString(id) else ""
descriptionTextView.text = description.toLowerCase()
setOnClickListener {
itemClickListener?.onClick(item)
}
}
}
}
}
\ No newline at end of file
......@@ -2,11 +2,16 @@ package chat.rocket.android.chatroom.adapter
import android.view.View
import chat.rocket.android.chatroom.viewmodel.ImageAttachmentViewModel
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.drawee.interfaces.DraweeController
import chat.rocket.android.widget.emoji.EmojiReactionListener
import com.stfalcon.frescoimageviewer.ImageViewer
import kotlinx.android.synthetic.main.message_attachment.view.*
class ImageAttachmentViewHolder(itemView: View, listener: ActionsListener)
: BaseViewHolder<ImageAttachmentViewModel>(itemView, listener) {
class ImageAttachmentViewHolder(itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<ImageAttachmentViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
......@@ -17,12 +22,21 @@ class ImageAttachmentViewHolder(itemView: View, listener: ActionsListener)
override fun bindViews(data: ImageAttachmentViewModel) {
with(itemView) {
image_attachment.setImageURI(data.attachmentUrl)
val controller = Fresco.newDraweeControllerBuilder().apply {
setUri(data.attachmentUrl)
autoPlayAnimations = true
oldController = image_attachment.controller
}.build()
image_attachment.controller = controller
file_name.text = data.attachmentTitle
image_attachment.setOnClickListener { view ->
// TODO - implement a proper image viewer with a proper Transition
val builder = ImageViewer.createPipelineDraweeControllerBuilder()
.setAutoPlayAnimations(true)
ImageViewer.Builder(view.context, listOf(data.attachmentUrl))
.setStartPosition(0)
.hideStatusBar(false)
.setCustomDraweeControllerBuilder(builder)
.show()
}
}
......
package chat.rocket.android.chatroom.adapter
import android.text.method.LinkMovementMethod
import android.view.View
import chat.rocket.android.chatroom.viewmodel.MessageAttachmentViewModel
import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.item_message.view.*
class MessageAttachmentViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null
) : BaseViewHolder<MessageAttachmentViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
text_content.movementMethod = LinkMovementMethod()
setupActionMenu(text_content)
}
}
override fun bindViews(data: MessageAttachmentViewModel) {
with(itemView) {
text_message_time.text = data.time
text_sender.text = data.senderName
text_content.text = data.content
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.chatroom.viewmodel.ReactionViewModel
import chat.rocket.android.dagger.DaggerLocalComponent
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.widget.emoji.Emoji
import chat.rocket.android.widget.emoji.EmojiListenerAdapter
import chat.rocket.android.widget.emoji.EmojiPickerPopup
import chat.rocket.android.widget.emoji.EmojiReactionListener
import java.util.concurrent.CopyOnWriteArrayList
import javax.inject.Inject
class MessageReactionsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
private const val REACTION_VIEW_TYPE = 0
private const val ADD_REACTION_VIEW_TYPE = 1
}
private val reactions = CopyOnWriteArrayList<ReactionViewModel>()
var listener: EmojiReactionListener? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view: View
return when (viewType) {
ADD_REACTION_VIEW_TYPE -> {
view = inflater.inflate(R.layout.item_add_reaction, parent, false)
AddReactionViewHolder(view, listener)
}
else -> {
view = inflater.inflate(R.layout.item_reaction, parent, false)
SingleReactionViewHolder(view, listener)
}
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is SingleReactionViewHolder) {
holder.bind(reactions[position])
} else {
holder as AddReactionViewHolder
holder.bind(reactions[0].messageId)
}
}
override fun getItemCount() = if (reactions.isEmpty()) 0 else reactions.size + 1
override fun getItemViewType(position: Int): Int {
if (position == reactions.size) {
return ADD_REACTION_VIEW_TYPE
}
return REACTION_VIEW_TYPE
}
fun addReactions(reactions: List<ReactionViewModel>) {
this.reactions.clear()
this.reactions.addAllAbsent(reactions)
notifyItemRangeInserted(0, reactions.size)
}
fun clear() {
val oldSize = reactions.size
reactions.clear()
notifyItemRangeRemoved(0, oldSize)
}
fun contains(reactionShortname: String) =
reactions.firstOrNull { it.shortname == reactionShortname} != null
class SingleReactionViewHolder(view: View,
private val listener: EmojiReactionListener?)
: RecyclerView.ViewHolder(view), View.OnClickListener {
@Inject lateinit var localRepository: LocalRepository
@Volatile lateinit var reaction: ReactionViewModel
@Volatile
var clickHandled = false
init {
DaggerLocalComponent.builder()
.context(itemView.context)
.build()
.inject(this)
}
fun bind(reaction: ReactionViewModel) {
clickHandled = false
this.reaction = reaction
with(itemView) {
val emojiTextView = findViewById<TextView>(R.id.text_emoji)
val countTextView = findViewById<TextView>(R.id.text_count)
emojiTextView.text = reaction.unicode
countTextView.text = reaction.count.toString()
val myself = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY)
if (reaction.usernames.contains(myself)) {
val context = itemView.context
val resources = context.resources
countTextView.setTextColor(resources.getColor(R.color.colorAccent))
}
emojiTextView.setOnClickListener(this@SingleReactionViewHolder)
countTextView.setOnClickListener(this@SingleReactionViewHolder)
}
}
override fun onClick(v: View?) {
synchronized(this) {
if (!clickHandled) {
clickHandled = true
listener?.onReactionTouched(reaction.messageId, reaction.shortname)
}
}
}
}
class AddReactionViewHolder(view: View,
private val listener: EmojiReactionListener?) : RecyclerView.ViewHolder(view) {
fun bind(messageId: String) {
itemView as ImageView
itemView.setOnClickListener {
val emojiPickerPopup = EmojiPickerPopup(itemView.context)
emojiPickerPopup.listener = object : EmojiListenerAdapter() {
override fun onEmojiAdded(emoji: Emoji) {
listener?.onReactionAdded(messageId, emoji)
}
}
emojiPickerPopup.show()
}
}
}
}
\ No newline at end of file
......@@ -3,13 +3,15 @@ package chat.rocket.android.chatroom.adapter
import android.text.method.LinkMovementMethod
import android.view.View
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_message.view.*
class MessageViewHolder(
itemView: View,
listener: ActionsListener
) : BaseViewHolder<MessageViewModel>(itemView, listener) {
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null
) : BaseViewHolder<MessageViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
......@@ -20,6 +22,9 @@ class MessageViewHolder(
override fun bindViews(data: MessageViewModel) {
with(itemView) {
if (data.isFirstUnread) new_messages_notif.visibility = View.VISIBLE
else new_messages_notif.visibility = View.GONE
text_message_time.text = data.time
text_sender.text = data.senderName
text_content.text = data.content
......
package chat.rocket.android.chatroom.adapter
import DrawableHelper
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.PeopleSuggestionsAdapter.PeopleSuggestionViewHolder
import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewModel
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.ui.BaseSuggestionViewHolder
import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter
import com.facebook.drawee.view.SimpleDraweeView
class PeopleSuggestionsAdapter(context: Context) : SuggestionsAdapter<PeopleSuggestionViewHolder>("@") {
init {
val allDescription = context.getString(R.string.suggest_all_description)
val hereDescription = context.getString(R.string.suggest_here_description)
val pinnedList = listOf(
PeopleSuggestionViewModel(imageUri = null,
text = "all",
username = "all",
name = allDescription,
status = null,
pinned = false,
searchList = listOf("all")),
PeopleSuggestionViewModel(imageUri = null,
text = "here",
username = "here",
name = hereDescription,
status = null,
pinned = false,
searchList = listOf("here"))
)
setPinnedSuggestions(pinnedList)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PeopleSuggestionViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.suggestion_member_item, parent,
false)
return PeopleSuggestionViewHolder(view)
}
class PeopleSuggestionViewHolder(view: View) : BaseSuggestionViewHolder(view) {
override fun bind(item: SuggestionModel, itemClickListener: SuggestionsAdapter.ItemClickListener?) {
item as PeopleSuggestionViewModel
with(itemView) {
val username = itemView.findViewById<TextView>(R.id.text_username)
val name = itemView.findViewById<TextView>(R.id.text_name)
val avatar = itemView.findViewById<SimpleDraweeView>(R.id.image_avatar)
val statusView = itemView.findViewById<ImageView>(R.id.image_status)
username.text = item.username
name.text = item.name
if (item.imageUri?.isEmpty() != false) {
avatar.setVisible(false)
} else {
avatar.setVisible(true)
avatar.setImageURI(item.imageUri)
}
val status = item.status
if (status != null) {
val statusDrawable = DrawableHelper.getUserStatusDrawable(status, itemView.context)
statusView.setImageDrawable(statusDrawable)
} else {
statusView.setVisible(false)
}
setOnClickListener {
itemClickListener?.onClick(item)
}
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.RoomSuggestionsAdapter.RoomSuggestionsViewHolder
import chat.rocket.android.chatroom.viewmodel.suggestion.ChatRoomSuggestionViewModel
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.ui.BaseSuggestionViewHolder
import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter
class RoomSuggestionsAdapter : SuggestionsAdapter<RoomSuggestionsViewHolder>("#") {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RoomSuggestionsViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.suggestion_room_item, parent,
false)
return RoomSuggestionsViewHolder(view)
}
class RoomSuggestionsViewHolder(view: View) : BaseSuggestionViewHolder(view) {
override fun bind(item: SuggestionModel, itemClickListener: SuggestionsAdapter.ItemClickListener?) {
item as ChatRoomSuggestionViewModel
with(itemView) {
val fullname = itemView.findViewById<TextView>(R.id.text_fullname)
val name = itemView.findViewById<TextView>(R.id.text_name)
name.text = item.name
fullname.text = item.fullName
setOnClickListener {
itemClickListener?.onClick(item)
}
}
}
}
}
\ No newline at end of file
......@@ -6,10 +6,13 @@ import android.view.View
import chat.rocket.android.chatroom.viewmodel.UrlPreviewViewModel
import chat.rocket.android.util.extensions.content
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.message_url_preview.view.*
class UrlPreviewViewHolder(itemView: View, listener: ActionsListener)
: BaseViewHolder<UrlPreviewViewModel>(itemView, listener) {
class UrlPreviewViewHolder(itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<UrlPreviewViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
......
......@@ -4,10 +4,13 @@ import android.view.View
import chat.rocket.android.chatroom.viewmodel.VideoAttachmentViewModel
import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.message_attachment.view.*
class VideoAttachmentViewHolder(itemView: View, listener: ActionsListener)
: BaseViewHolder<VideoAttachmentViewModel>(itemView, listener) {
class VideoAttachmentViewHolder(itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<VideoAttachmentViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
......
......@@ -2,6 +2,9 @@ package chat.rocket.android.chatroom.presentation
import android.net.Uri
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.chatroom.viewmodel.suggestion.ChatRoomSuggestionViewModel
import chat.rocket.android.chatroom.viewmodel.suggestion.CommandSuggestionViewModel
import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewModel
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.core.internal.realtime.State
......@@ -100,4 +103,19 @@ interface ChatRoomView : LoadingView, MessageView {
fun showInvalidFileSize(fileSize: Int, maxFileSize: Int)
fun showConnectionState(state: State)
fun populatePeopleSuggestions(members: List<PeopleSuggestionViewModel>)
fun populateRoomSuggestions(chatRooms: List<ChatRoomSuggestionViewModel>)
/**
* This user has joined the chat callback.
*/
fun onJoined()
fun showReactionsPopup(messageId: String)
/**
* Show list of commands.
*
* @param commands The list of available commands.
*/
fun populateCommandSuggestions(commands: List<CommandSuggestionViewModel>)
}
\ No newline at end of file
......@@ -24,7 +24,7 @@ class PinnedMessagesPresenter @Inject constructor(private val view: PinnedMessag
getSettingsInteractor: GetSettingsInteractor) {
private val client = factory.create(serverInteractor.get()!!)
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!!
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)
private var pinnedMessagesListOffset: Int = 0
/**
......
package chat.rocket.android.chatroom.ui
import android.graphics.drawable.Drawable
import android.support.design.widget.BaseTransientBottomBar
import android.support.v4.view.ViewCompat
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
......@@ -27,7 +27,6 @@ class ActionSnackbar : BaseTransientBottomBar<ActionSnackbar> {
actionSnackbar.cancelView = view.findViewById(R.id.image_view_action_cancel_quote) as ImageView
actionSnackbar.duration = BaseTransientBottomBar.LENGTH_INDEFINITE
val spannable = Markwon.markdown(context, content).trim()
actionSnackbar.marginDrawable = context.getDrawable(R.drawable.quote)
actionSnackbar.messageTextView.content = spannable
return actionSnackbar
}
......@@ -37,19 +36,16 @@ class ActionSnackbar : BaseTransientBottomBar<ActionSnackbar> {
lateinit var cancelView: View
private lateinit var messageTextView: TextView
private lateinit var titleTextView: TextView
private lateinit var marginDrawable: Drawable
var text: String = ""
set(value) {
val spannable = parser.renderMarkdown(value) as Spannable
spannable.setSpan(MessageParser.QuoteMarginSpan(marginDrawable, 10), 0, spannable.length, 0)
val spannable = SpannableStringBuilder.valueOf(value)
messageTextView.content = spannable
}
var title: String = ""
set(value) {
val spannable = Markwon.markdown(this.context, value) as Spannable
spannable.setSpan(MessageParser.QuoteMarginSpan(marginDrawable, 10), 0, spannable.length, 0)
titleTextView.content = spannable
}
......@@ -68,17 +64,17 @@ class ActionSnackbar : BaseTransientBottomBar<ActionSnackbar> {
override fun animateContentOut(delay: Int, duration: Int) {
ViewCompat.setScaleY(content, 1f)
ViewCompat.animate(content)
.scaleY(0f)
.setDuration(duration.toLong())
.startDelay = delay.toLong()
.scaleY(0f)
.setDuration(duration.toLong())
.startDelay = delay.toLong()
}
override fun animateContentIn(delay: Int, duration: Int) {
ViewCompat.setScaleY(content, 0f)
ViewCompat.animate(content)
.scaleY(1f)
.setDuration(duration.toLong())
.startDelay = delay.toLong()
.scaleY(1f)
.setDuration(duration.toLong())
.startDelay = delay.toLong()
}
}
}
\ No newline at end of file
......@@ -20,14 +20,22 @@ import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.app_bar_chat_room.*
import javax.inject.Inject
import timber.log.Timber
fun Context.chatRoomIntent(chatRoomId: String, chatRoomName: String, chatRoomType: String, isChatRoomReadOnly: Boolean): Intent {
fun Context.chatRoomIntent(chatRoomId: String,
chatRoomName: String,
chatRoomType: String,
isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long,
isChatRoomSubscribed: Boolean = true): Intent {
return Intent(this, ChatRoomActivity::class.java).apply {
putExtra(INTENT_CHAT_ROOM_ID, chatRoomId)
putExtra(INTENT_CHAT_ROOM_NAME, chatRoomName)
putExtra(INTENT_CHAT_ROOM_TYPE, chatRoomType)
putExtra(INTENT_IS_CHAT_ROOM_READ_ONLY, isChatRoomReadOnly)
putExtra(INTENT_CHAT_ROOM_LAST_SEEN, chatRoomLastSeen)
putExtra(INTENT_CHAT_IS_SUBSCRIBED, isChatRoomSubscribed)
}
}
......@@ -35,6 +43,8 @@ private const val INTENT_CHAT_ROOM_ID = "chat_room_id"
private const val INTENT_CHAT_ROOM_NAME = "chat_room_name"
private const val INTENT_CHAT_ROOM_TYPE = "chat_room_type"
private const val INTENT_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only"
private const val INTENT_CHAT_ROOM_LAST_SEEN = "chat_room_last_seen"
private const val INTENT_CHAT_IS_SUBSCRIBED = "is_chat_room_subscribed"
class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
......@@ -47,6 +57,8 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
private lateinit var chatRoomName: String
private lateinit var chatRoomType: String
private var isChatRoomReadOnly: Boolean = false
private var isChatRoomSubscribed: Boolean = true
private var chatRoomLastSeen: Long = -1L
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
......@@ -70,8 +82,15 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
setupToolbar()
addFragment("ChatRoomFragment", R.id.fragment_container) {
newInstance(chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly)
chatRoomLastSeen = intent.getLongExtra(INTENT_CHAT_ROOM_LAST_SEEN, -1)
isChatRoomSubscribed = intent.getBooleanExtra(INTENT_CHAT_IS_SUBSCRIBED, true)
if (supportFragmentManager.findFragmentByTag("ChatRoomFragment") == null) {
addFragment("ChatRoomFragment", R.id.fragment_container) {
newInstance(chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly, chatRoomLastSeen,
isChatRoomSubscribed)
}
}
}
......@@ -83,31 +102,39 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
return fragmentDispatchingAndroidInjector
}
fun showRoomTypeIcon(showRoomTypeIcon: Boolean) {
if (showRoomTypeIcon) {
val roomType = roomTypeOf(chatRoomType)
val drawable = when (roomType) {
is RoomType.Channel -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_channel, this)
}
is RoomType.PrivateGroup -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_lock, this)
}
is RoomType.DirectMessage -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_dm, this)
}
else -> null
}
drawable?.let {
val wrappedDrawable = DrawableHelper.wrapDrawable(it)
val mutableDrawable = wrappedDrawable.mutate()
DrawableHelper.tintDrawable(mutableDrawable, this, R.color.white)
DrawableHelper.compoundDrawable(text_room_name, mutableDrawable)
}
} else {
text_room_name.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
}
}
private fun setupToolbar() {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
text_room_name.textContent = chatRoomName
val roomType = roomTypeOf(chatRoomType)
val drawable = when (roomType) {
is RoomType.Channel -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_channel, this)
}
is RoomType.PrivateGroup -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_lock, this)
}
is RoomType.DirectMessage -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_dm, this)
}
else -> null
}
drawable?.let {
val wrappedDrawable = DrawableHelper.wrapDrawable(it)
val mutableDrawable = wrappedDrawable.mutate()
DrawableHelper.tintDrawable(mutableDrawable, this, R.color.white)
DrawableHelper.compoundDrawable(text_room_name, mutableDrawable)
}
showRoomTypeIcon(true)
toolbar.setNavigationOnClickListener {
finishActivity()
......
......@@ -66,9 +66,13 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
override fun hideLoading() = view_loading.setVisible(false)
override fun showMessage(resId: Int) = showToast(resId)
override fun showMessage(resId: Int) {
showToast(resId)
}
override fun showMessage(message: String) = showToast(message)
override fun showMessage(message: String) {
showToast(message)
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
......
......@@ -10,7 +10,10 @@ data class AudioAttachmentViewModel(
override val messageId: String,
override val attachmentUrl: String,
override val attachmentTitle: CharSequence,
override val id: Long
override val id: Long,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
) : BaseFileAttachmentViewModel<AudioAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.AUDIO_ATTACHMENT.viewType
......
......@@ -9,6 +9,9 @@ interface BaseViewModel<out T> {
val messageId: String
val viewType: Int
val layoutId: Int
var reactions: List<ReactionViewModel>
var nextDownStreamMessage: BaseViewModel<*>?
var preview: Message?
enum class ViewType(val viewType: Int) {
MESSAGE(0),
......
......@@ -10,7 +10,10 @@ data class ImageAttachmentViewModel(
override val messageId: String,
override val attachmentUrl: String,
override val attachmentTitle: CharSequence,
override val id: Long
override val id: Long,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
) : BaseFileAttachmentViewModel<ImageAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.IMAGE_ATTACHMENT.viewType
......
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
data class MessageAttachmentViewModel(
override val message: Message,
override val rawData: Message,
override val messageId: String,
var senderName: String?,
val time: CharSequence?,
val content: CharSequence,
val isPinned: Boolean,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
var messageLink: String? = null,
override var preview: Message? = null
) : BaseViewModel<Message> {
override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_message_attachment
}
\ No newline at end of file
......@@ -11,7 +11,11 @@ data class MessageViewModel(
override val time: CharSequence,
override val senderName: CharSequence,
override val content: CharSequence,
override val isPinned: Boolean
override val isPinned: Boolean,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null,
var isFirstUnread: Boolean
) : BaseMessageViewModel<Message> {
override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE.viewType
......
package chat.rocket.android.chatroom.viewmodel
data class ReactionViewModel(
val messageId: String,
val shortname: String,
val unicode: CharSequence,
val count: Int,
val usernames: List<String> = emptyList()
)
\ No newline at end of file
......@@ -5,13 +5,16 @@ import chat.rocket.core.model.Message
import chat.rocket.core.model.url.Url
data class UrlPreviewViewModel(
override val message: Message,
override val rawData: Url,
override val messageId: String,
val title: CharSequence?,
val hostname: String,
val description: CharSequence?,
val thumbUrl: String?
override val message: Message,
override val rawData: Url,
override val messageId: String,
val title: CharSequence?,
val hostname: String,
val description: CharSequence?,
val thumbUrl: String?,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
) : BaseViewModel<Url> {
override val viewType: Int
get() = BaseViewModel.ViewType.URL_PREVIEW.viewType
......
......@@ -10,7 +10,10 @@ data class VideoAttachmentViewModel(
override val messageId: String,
override val attachmentUrl: String,
override val attachmentTitle: CharSequence,
override val id: Long
override val id: Long,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
) : BaseFileAttachmentViewModel<VideoAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.VIDEO_ATTACHMENT.viewType
......
package chat.rocket.android.chatroom.viewmodel.suggestion
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
class ChatRoomSuggestionViewModel(text: String,
val fullName: String,
val name: String,
searchList: List<String>) : SuggestionModel(text, searchList, false) {
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel.suggestion
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
class CommandSuggestionViewModel(text: String,
val description: String,
searchList: List<String>) : SuggestionModel(text, searchList)
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel.suggestion
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.common.model.UserStatus
class PeopleSuggestionViewModel(val imageUri: String?,
text: String,
val username: String,
val name: String,
val status: UserStatus?,
pinned: Boolean = false,
searchList: List<String>) : SuggestionModel(text, searchList, pinned) {
override fun toString(): String {
return "PeopleSuggestionViewModel(imageUri='$imageUri', username='$username', name='$name', status=$status, pinned=$pinned)"
}
}
\ No newline at end of file
......@@ -3,11 +3,9 @@ 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
......@@ -22,9 +20,4 @@ class ChatRoomsFragmentModule {
fun provideLifecycleOwner(frag: ChatRoomsFragment): 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.chatrooms.di
import android.content.Context
import chat.rocket.android.chatrooms.presentation.ChatRoomsNavigator
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.dagger.scope.PerActivity
import dagger.Module
import dagger.Provides
@Module
class ChatRoomsModule {
@Provides
@PerActivity
fun provideChatRoomsNavigator(activity: MainActivity, context: Context) = ChatRoomsNavigator(activity, context)
}
\ No newline at end of file
package chat.rocket.android.chatrooms.presentation
import android.content.Context
import chat.rocket.android.R
import chat.rocket.android.chatroom.ui.chatRoomIntent
import chat.rocket.android.main.ui.MainActivity
class ChatRoomsNavigator(private val activity: MainActivity, private val context: Context) {
fun toChatRoom(chatRoomId: String, chatRoomName: String, chatRoomType: String, isChatRoomReadOnly: Boolean) {
activity.startActivity(context.chatRoomIntent(chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly))
activity.overridePendingTransition(R.anim.open_enter, R.anim.open_exit)
}
}
\ No newline at end of file
......@@ -4,6 +4,7 @@ import android.app.Application
import chat.rocket.android.app.RocketChatApplication
import chat.rocket.android.dagger.module.ActivityBuilder
import chat.rocket.android.dagger.module.AppModule
import chat.rocket.android.dagger.module.ReceiverBuilder
import chat.rocket.android.dagger.module.ServiceBuilder
import chat.rocket.android.push.FirebaseTokenService
import dagger.BindsInstance
......@@ -12,7 +13,8 @@ import dagger.android.support.AndroidSupportInjectionModule
import javax.inject.Singleton
@Singleton
@Component(modules = [AndroidSupportInjectionModule::class, AppModule::class, ActivityBuilder::class, ServiceBuilder::class])
@Component(modules = [AndroidSupportInjectionModule::class,
AppModule::class, ActivityBuilder::class, ServiceBuilder::class, ReceiverBuilder::class])
interface AppComponent {
@Component.Builder
......
package chat.rocket.android.dagger
import android.content.Context
import chat.rocket.android.chatroom.adapter.MessageReactionsAdapter
import chat.rocket.android.dagger.module.LocalModule
import dagger.BindsInstance
import dagger.Component
import javax.inject.Singleton
@Singleton
@Component(modules = [LocalModule::class])
interface LocalComponent {
@Component.Builder
interface Builder {
@BindsInstance
fun context(applicationContext: Context): Builder
fun build(): LocalComponent
}
fun inject(adapter: MessageReactionsAdapter.SingleReactionViewHolder)
fun inject(adapter: MessageReactionsAdapter.AddReactionViewHolder)
/*@Component.Builder
abstract class Builder : AndroidInjector.Builder<RocketChatApplication>()*/
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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