Unverified Commit 96e9dda2 authored by Leonardo Aramaki's avatar Leonardo Aramaki Committed by GitHub

Merge branch 'develop-2.x' into newPR-update-readme

parents b573e5cf e38a52ce
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
#!/bin/bash
CURRENT_DIR=`pwd`
CURRENT_DIR=$(pwd)
# The SDK dir should be 2 directories up in the tree, so we use dirname 2 times
# to get the common parent dir of the SDK and the app
GIT=`which git`
tmp=`dirname $CURRENT_DIR`
tmp=`dirname $tmp`
GIT=$(which git)
cd ../..
tmp=$(pwd)
SDK_DIR="$tmp/Rocket.Chat.Kotlin.SDK"
cd "${CURRENT_DIR}"
echo "CURRENT DIR: $CURRENT_DIR"
echo "SDK DIR: $SDK_DIR"
# check if there are changes not commited
# check if there are changes not committed
function git_stat {
local __resultvar=$1
cd $SDK_DIR && $GIT diff --shortstat --exit-code
eval $__resultvar="'$?'"
local resultVar=$1
cd "${SDK_DIR}" && "${GIT}" diff --shortstat --exit-code
eval ${resultVar}="'$?'"
}
# check for changes already on the index not commited
# check for changes already on the index not committed
function git_stat_cached {
local __resultvar=$1
cd $SDK_DIR && $GIT diff --cached --shortstat --exit-code
eval $__resultvar="'$?'"
local resultVar=$1
cd "${SDK_DIR}" && "${GIT}" diff --cached --shortstat --exit-code
eval ${resultVar}="'$?'"
}
# get the SHA of the lastest commit
# get the SHA of the latest commit
function git_sha {
temp_sha=`cd $SDK_DIR && $GIT rev-parse --short HEAD`
temp_sha=$(cd "${SDK_DIR}" && "${GIT}" rev-parse --short HEAD)
echo "$temp_sha"
}
function git_app_branch {
temp_branch=`cd $CURRENT_DIR && $GIT rev-parse --abbrev-ref HEAD`
temp_branch=$(cd "${CURRENT_DIR}" && "${GIT}" rev-parse --abbrev-ref HEAD)
echo "$temp_branch"
}
# check if the tree is dirty (has modifications not commited yet)
# check if the tree is dirty (has modifications not committed yet)
function check_git_dirty {
git_stat stat
git_stat_cached cached
if [ $stat -eq 0 ] && [ $cached -eq 0 ]; then
if [ ${stat} -eq 0 ] && [ ${cached} -eq 0 ]; then
echo "not dirty"
return 1
else
......@@ -52,11 +53,11 @@ function check_git_dirty {
# check if the saved last commit is the same as the latest SHA in the tree
function check_last_commit {
if [ ! -f $SDK_DIR/.last_commit_hash ]; then
if [ ! -f ${SDK_DIR}/.last_commit_hash ]; then
echo "last_commit_hash not found"
return 0
fi
saved_hash=`cat $SDK_DIR/.last_commit_hash`
saved_hash=$(cat "${SDK_DIR}"/.last_commit_hash)
last_hash=$(git_sha)
#`cd $SDK_DIR && git rev-parse --short HEAD`
if [ "$saved_hash" == "$last_hash" ]; then
......@@ -70,7 +71,7 @@ function check_last_commit {
function checkout_matching_branch {
app_branch=$(git_app_branch)
cd $SDK_DIR && $GIT checkout $app_branch 1>/dev/null 2>/dev/null
cd "${SDK_DIR}" && "${GIT}" checkout "${app_branch}" 1>/dev/null 2>/dev/null
}
checkout_matching_branch
......@@ -82,20 +83,20 @@ fi
echo "CURRENT SHA: $SHA"
# if the tree is not dirty, there is no new commit and the .jars are still there, just skip the build
if ! check_git_dirty && ! check_last_commit && [ -f $CURRENT_DIR/libs/common-$SHA.jar ] && [ -f $CURRENT_DIR/libs/core-$SHA.jar ]; then
if ! check_git_dirty && ! check_last_commit && [ -f "${CURRENT_DIR}"/libs/common-"${SHA}".jar ] && [ -f "${CURRENT_DIR}"/libs/core-"${SHA}".jar ]; then
echo "NO BUILD NEEDED"
exit 0
fi
cd $SDK_DIR && ./gradlew common:assemble && cd $CURRENT_DIR
cd $SDK_DIR && ./gradlew core:assemble && cd $CURRENT_DIR
cd "${SDK_DIR}" && ./gradlew common:assemble && cd "${CURRENT_DIR}"
cd "${SDK_DIR}" && ./gradlew core:assemble && cd "${CURRENT_DIR}"
rm $CURRENT_DIR/libs/common* $CURRENT_DIR/libs/core*
rm "${CURRENT_DIR}"/libs/common* "${CURRENT_DIR}"/libs/core*
mkdir -p $CURRENT_DIR/libs/
cp -v $SDK_DIR/common/build/libs/common-0.1-SNAPSHOT.jar $CURRENT_DIR/libs/common-$SHA.jar
cp -v $SDK_DIR/core/build/libs/core-0.1-SNAPSHOT.jar $CURRENT_DIR/libs/core-$SHA.jar
mkdir -p "${CURRENT_DIR}"/libs/
cp -v "${SDK_DIR}"/common/build/libs/common-0.1-SNAPSHOT.jar "${CURRENT_DIR}"/libs/common-"${SHA}".jar
cp -v "${SDK_DIR}"/core/build/libs/core-0.1-SNAPSHOT.jar "${CURRENT_DIR}"/libs/core-"${SHA}".jar
echo "$SHA" > $SDK_DIR/.last_commit_hash
echo "$SHA" > "${SDK_DIR}"/.last_commit_hash
exit 0
\ No newline at end of file
......@@ -12,8 +12,8 @@ android {
applicationId "chat.rocket.android"
minSdkVersion 21
targetSdkVersion versions.targetSdk
versionCode 1008
versionName "2.0.0-dev6"
versionCode 2000
versionName "2.0.0-alpha1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}
......@@ -32,7 +32,6 @@ android {
signingConfig signingConfigs.release
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
applicationIdSuffix ".dev"
}
debug {
......@@ -59,6 +58,9 @@ dependencies {
implementation libraries.design
implementation libraries.constraintLayout
implementation libraries.cardView
implementation libraries.flexbox
implementation libraries.androidKtx
implementation libraries.dagger
implementation libraries.daggerSupport
......@@ -93,20 +95,13 @@ dependencies {
implementation libraries.frescoImageViewer
implementation libraries.androidSvg
implementation libraries.aVLoadingIndicatorView
implementation libraries.textDrawable
implementation libraries.markwon
implementation libraries.markwonImageLoader
implementation libraries.moshiLazyAdapters
implementation libraries.sheetMenu
implementation libraries.aVLoadingIndicatorView
implementation('com.crashlytics.sdk.android:crashlytics:2.6.8@aar') {
transitive = true
}
......@@ -126,7 +121,11 @@ kotlin {
// FIXME - build and install the sdk into the app/libs directory
// We were having some issues with the kapt generated files from the sdk when importing as a module
task compileSdk(type:Exec) {
commandLine './build-sdk.sh'
if (System.getProperty('os.name').toLowerCase(Locale.ROOT).contains('windows')) {
commandLine 'cmd', '/c', 'build-sdk.sh'
} else {
commandLine './build-sdk.sh'
}
}
preBuild.dependsOn compileSdk
......
#Thu Feb 15 15:50:42 BRST 2018
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
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
......@@ -37,18 +37,31 @@
<activity
android:name=".main.ui.MainActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" />
<activity
android:name=".webview.WebViewActivity"
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=".chatroom.ui.ChatRoomActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" />
<activity
android:name=".chatroom.ui.PinnedMessagesActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" />
<activity
android:name=".settings.password.ui.PasswordActivity"
android:theme="@style/AppTheme" />
<receiver
......
This source diff could not be displayed because it is too large. You can view the blob instead.
import android.content.Context
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.support.v4.content.ContextCompat
import android.support.v4.graphics.drawable.DrawableCompat
import android.widget.EditText
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.helper.TextHelper
import com.amulyakhare.textdrawable.TextDrawable
import chat.rocket.common.model.UserStatus
object DrawableHelper {
private val AVATAR_BACKGROUND_HEXADECIMAL_COLORS = intArrayOf(
0xFFF44336.toInt(), 0xFFE91E63.toInt(), 0xFF9C27B0.toInt(), 0xFF673AB7.toInt(), 0xFF3F51B5.toInt(),
0xFF2196F3.toInt(), 0xFF03A9F4.toInt(), 0xFF00BCD4.toInt(), 0xFF009688.toInt(), 0xFF4CAF50.toInt(),
0xFF8BC34A.toInt(), 0xFFCDDC39.toInt(), 0xFFFFC107.toInt(), 0xFFFF9800.toInt(), 0xFFFF5722.toInt(),
0xFF795548.toInt(), 0xFF9E9E9E.toInt(), 0xFF607D8B.toInt())
/**
* Returns a Drawable from its ID.
*
......@@ -85,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 {
......@@ -111,41 +105,16 @@ 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
}
/**
* Returns a drawable with the first character from a string.
*
* @param string The string to get its first character and to get the avatar background color.
* @return A drawable with the string first character.
*/
fun getTextDrawable(string: String): Drawable {
return TextDrawable.builder()
.beginConfig()
.useFont(Typeface.SANS_SERIF)
.endConfig()
.buildRoundRect(TextHelper.getFirstCharacter(string), getAvatarBackgroundColor(string), 4)
}
/**
* Returns a background color to be rendered on the avatar.
*
* @param string Gets the background color based on the provided string.
* @return A hexadecimal color.
* @see (Rocket.Chat/server/startup/avatar.js)
*/
private fun getAvatarBackgroundColor(string: String): Int {
return AVATAR_BACKGROUND_HEXADECIMAL_COLORS[string.length % AVATAR_BACKGROUND_HEXADECIMAL_COLORS.size]
}
}
\ No newline at end of file
......@@ -9,6 +9,7 @@ import chat.rocket.android.helper.CrashlyticsTree
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.MultiServerTokenRepository
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.widget.emoji.EmojiRepository
import chat.rocket.common.model.Token
import chat.rocket.core.TokenRepository
import com.crashlytics.android.Crashlytics
......@@ -57,6 +58,7 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
initCurrentServer()
AndroidThreeTen.init(this)
EmojiRepository.load(this)
setupCrashlytics()
setupFresco()
......
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.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 = "<?xml"
private val possibleHeaderTags = arrayOf(ImageFormatCheckerUtils.asciiBytes("<svg"))
/**
* 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
......@@ -6,6 +6,6 @@ import dagger.android.ContributesAndroidInjector
@Module abstract class LoginFragmentProvider {
@ContributesAndroidInjector(modules = [(LoginFragmentModule::class)])
@ContributesAndroidInjector(modules = [LoginFragmentModule::class])
abstract fun provideLoginFragment(): LoginFragment
}
\ No newline at end of file
......@@ -4,17 +4,20 @@ 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.*
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.generateRandomString
import chat.rocket.android.util.extensions.isEmailValid
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException
import chat.rocket.common.RocketChatTwoFactorException
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
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.*
import kotlinx.coroutines.experimental.delay
import java.util.concurrent.TimeUnit
import javax.inject.Inject
class LoginPresenter @Inject constructor(private val view: LoginView,
......@@ -28,51 +31,69 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
// TODO - we should validate the current server when opening the app, and have a nonnull get()
private val client: RocketChatClient = factory.create(serverInteractor.get()!!)
fun setup() {
fun setupView() {
val server = serverInteractor.get()
if (server == null) {
navigator.toServerScreen()
return
}
val settings = settingsInteractor.get(server)
if (settings == null) {
navigator.toServerScreen()
return
if (settings.isLoginFormEnabled()) {
view.showFormView()
view.setupLoginButtonListener()
view.setupGlobalListener()
} else {
view.hideFormView()
}
if (settings.isRegistrationEnabledForNewUsers()) {
view.showSignUpView()
view.setupSignUpView()
}
view.showSignUpView(settings.registrationEnabled())
if (settings.isCasAuthenticationEnabled()) {
val token = generateRandomString(17)
view.setupCasButtonListener(UrlHelper.getCasUrl(settings.casLoginUrl(), server, token), token)
view.showCasButton()
}
var hasSocial = false
if (settings.facebookEnabled()) {
var totalSocialAccountsEnabled = 0
if (settings.isFacebookAuthenticationEnabled()) {
view.enableLoginByFacebook()
hasSocial = true
totalSocialAccountsEnabled++
}
if (settings.githubEnabled()) {
if (settings.isGithubAuthenticationEnabled()) {
view.enableLoginByGithub()
hasSocial = true
totalSocialAccountsEnabled++
}
if (settings.googleEnabled()) {
if (settings.isGoogleAuthenticationEnabled()) {
view.enableLoginByGoogle()
hasSocial = true
totalSocialAccountsEnabled++
}
if (settings.linkedinEnabled()) {
if (settings.isLinkedinAuthenticationEnabled()) {
view.enableLoginByLinkedin()
hasSocial = true
totalSocialAccountsEnabled++
}
if (settings.meteorEnabled()) {
if (settings.isMeteorAuthenticationEnabled()) {
view.enableLoginByMeteor()
hasSocial = true
totalSocialAccountsEnabled++
}
if (settings.twitterEnabled()) {
if (settings.isTwitterAuthenticationEnabled()) {
view.enableLoginByTwitter()
hasSocial = true
totalSocialAccountsEnabled++
}
if (settings.gitlabEnabled()) {
if (settings.isGitlabAuthenticationEnabled()) {
view.enableLoginByGitlab()
hasSocial = true
totalSocialAccountsEnabled++
}
if (totalSocialAccountsEnabled > 0) {
view.showOauthView()
if (totalSocialAccountsEnabled > 3) {
view.setupFabListener()
}
}
view.showOauthView(hasSocial)
}
fun authenticate(usernameOrEmail: String, password: String) {
......@@ -90,16 +111,21 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
else -> {
launchUI(strategy) {
if (NetworkHelper.hasInternetAccess()) {
view.disableUserInput()
view.showLoading()
try {
val token = client.login(usernameOrEmail, password)
val me = client.me()
multiServerRepository.save(
server,
TokenModel(token.userId, token.authToken)
)
localRepository.save(LocalRepository.USERNAME_KEY, me.username)
val token = if (usernameOrEmail.isEmailValid()) {
client.loginWithEmail(usernameOrEmail, password)
} else {
val settings = settingsInteractor.get(server)
if (settings.isLdapAuthenticationEnabled()) {
client.loginWithLdap(usernameOrEmail, password)
} else {
client.login(usernameOrEmail, password)
}
}
saveToken(server, TokenModel(token.userId, token.authToken), client.me().username)
registerPushToken()
navigator.toChatList()
} catch (exception: RocketChatException) {
......@@ -117,6 +143,7 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
}
} finally {
view.hideLoading()
view.enableUserInput()
}
} else {
view.showNoInternetConnection()
......@@ -126,8 +153,46 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
}
}
fun authenticateWithCas(casToken: String) {
launchUI(strategy) {
if (NetworkHelper.hasInternetAccess()) {
view.disableUserInput()
view.showLoading()
try {
val server = serverInteractor.get()
if (server != null) {
delay(3, TimeUnit.SECONDS)
val token = client.loginWithCas(casToken)
saveToken(server, TokenModel(token.userId, token.authToken), client.me().username)
registerPushToken()
navigator.toChatList()
} else {
navigator.toServerScreen()
}
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
view.enableUserInput()
}
} else {
view.showNoInternetConnection()
}
}
}
fun signup() = navigator.toSignUp()
private suspend fun saveToken(server: String, tokenModel: TokenModel, username: String?) {
multiServerRepository.save(server, tokenModel)
localRepository.save(LocalRepository.USERNAME_KEY, username)
registerPushToken()
}
private suspend fun registerPushToken() {
localRepository.get(LocalRepository.KEY_PUSH_TOKEN)?.let {
client.registerPushToken(it)
......
......@@ -7,62 +7,135 @@ import chat.rocket.android.core.behaviours.MessageView
interface LoginView : LoadingView, MessageView, InternetView {
/**
* 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 [setupCasButtonListener].
*/
fun showCasButton()
/**
* Hides the CAS button.
*/
fun hideCasButton()
/**
* Setups the CAS button when tapped.
*
* @param casUrl The CAS URL to login/sign up with.
* @param casToken The requested Token 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()
/**
* Shows the oauth view if the login via social accounts is 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 showOauthView()
/**
* Setups the FloatingActionButton to show more social accounts views (expanding the oauth view interface to show the remaining view(s)).
* Hides the oauth view.
*/
fun setupFabListener()
fun hideOauthView()
/**
* 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 enabled by the server settings.
*/
fun enableLoginByFacebook()
/**
* Shows the login by Github view.
* Shows the "login by Github" view if it is enabled by the server settings.
*/
fun enableLoginByGithub()
/**
* Shows the login by Google view.
* Shows the "login by Google" view if it is enabled by the server settings.
*/
fun enableLoginByGoogle()
/**
* Shows the login by Linkedin view.
* Shows the "login by Linkedin" view if it is enabled by the server settings.
*/
fun enableLoginByLinkedin()
/**
* Shows the login by Meteor view.
* Shows the "login by Meteor" view if it is enabled by the server settings.
*/
fun enableLoginByMeteor()
/**
* Shows the login by Twitter view.
* Shows the "login by Twitter" view if it is enabled by the server settings.
*/
fun enableLoginByTwitter()
/**
* Shows the login by Gitlab view.
* Shows the "login by Gitlab" view if it is enabled by the server settings.
*/
fun enableLoginByGitlab()
/**
* Shows the sign up view if the server settings allow the new users registration.
*
* @param value True to show the sign up view, false otherwise.
* Setups the FloatingActionButton to show more social accounts views (expanding the oauth view interface to show the remaining view(s)).
*/
fun showSignUpView(value: Boolean)
fun setupFabListener()
fun setupGlobalListener()
/**
* Alerts the user about a wrong inputted username or email.
......
package chat.rocket.android.authentication.login.ui
import DrawableHelper
import android.content.Context
import android.app.Activity
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.support.v4.app.Fragment
......@@ -15,33 +16,20 @@ import android.widget.ScrollView
import chat.rocket.android.R
import chat.rocket.android.authentication.login.presentation.LoginPresenter
import chat.rocket.android.authentication.login.presentation.LoginView
import chat.rocket.android.helper.AnimationHelper
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.TextHelper
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.*
import chat.rocket.android.webview.cas.ui.webViewIntent
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_log_in.*
import javax.inject.Inject
internal const val REQUEST_CODE_FOR_CAS = 1
class LoginFragment : Fragment(), LoginView {
@Inject lateinit var presenter: LoginPresenter
@Inject lateinit var appContext: Context // TODO we really need it? Check alternatives...
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
if (KeyboardHelper.isSoftKeyboardShown(scroll_view.rootView)) {
showSignUpView(false)
showOauthView(false)
showLoginButton(true)
} else {
if (isEditTextEmpty()) {
showSignUpView(true)
showOauthView(true)
showLoginButton(false)
}
}
areLoginOptionsNeeded()
}
private var isGlobalLayoutListenerSetUp = false
......@@ -54,7 +42,8 @@ class LoginFragment : Fragment(), LoginView {
AndroidSupportInjection.inject(this)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_authentication_log_in)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
container?.inflate(R.layout.fragment_authentication_log_in)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
......@@ -63,26 +52,7 @@ class LoginFragment : Fragment(), LoginView {
tintEditTextDrawableStart()
}
presenter.setup()
showThreeSocialMethods()
button_log_in.setOnClickListener {
presenter.authenticate(text_username_or_email.textContent, text_password.textContent)
}
setupFabListener()
setupSignUpListener()
}
private fun showThreeSocialMethods() {
var count = 0
for (i in 0..social_accounts_container.childCount) {
val view = social_accounts_container.getChildAt(i) as? ImageButton ?: continue
if (view.isEnabled && count < 3) {
view.visibility = View.VISIBLE
count++
}
}
presenter.setupView()
}
override fun onDestroyView() {
......@@ -93,110 +63,100 @@ class LoginFragment : Fragment(), LoginView {
}
}
override fun showOauthView(value: Boolean) {
if (value) {
social_accounts_container.setVisible(true)
button_fab.setVisible(true)
// We need to setup the layout to hide and show the oauth interface when the soft keyboard is shown
// (means that the user touched the text_username_or_email or text_password EditText to fill that respective fields).
if (!isGlobalLayoutListenerSetUp) {
scroll_view.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
isGlobalLayoutListenerSetUp = true
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_FOR_CAS) {
if (resultCode == Activity.RESULT_OK) {
data?.apply {
presenter.authenticateWithCas(getStringExtra("cas_token"))
}
}
} else {
social_accounts_container.setVisible(false)
button_fab.setVisible(false)
}
}
override fun setupFabListener() {
button_fab.setOnClickListener({
button_fab.hide()
showRemainingSocialAccountsView()
scrollToBottom()
})
}
private fun tintEditTextDrawableStart() {
activity?.apply {
val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_assignment_ind_black_24dp, this)
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_24dp, this)
override fun enableLoginByFacebook() {
button_facebook.isEnabled = true
val drawables = arrayOf(personDrawable, lockDrawable)
DrawableHelper.wrapDrawables(drawables)
DrawableHelper.tintDrawables(drawables, this, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawables(arrayOf(text_username_or_email, text_password), drawables)
}
}
override fun enableLoginByGithub() {
button_github.isEnabled = true
override fun showLoading() {
view_loading.setVisible(true)
}
override fun enableLoginByGoogle() {
button_google.isEnabled = true
override fun hideLoading() {
view_loading.setVisible(false)
}
override fun enableLoginByLinkedin() {
button_linkedin.isEnabled = true
override fun showNoInternetConnection() {
showMessage(R.string.msg_no_internet_connection)
}
override fun enableLoginByMeteor() {
button_meteor.isEnabled = true
override fun showMessage(resId: Int) {
showToast(resId)
}
override fun enableLoginByTwitter() {
button_twitter.isEnabled = true
override fun showMessage(message: String) {
showToast(message)
}
override fun enableLoginByGitlab() {
button_gitlab.isEnabled = true
override fun showGenericErrorMessage() {
showMessage(R.string.msg_generic_error)
}
override fun showSignUpView(value: Boolean) = text_new_to_rocket_chat.setVisible(value)
override fun alertWrongUsernameOrEmail() {
AnimationHelper.vibrateSmartPhone(appContext)
AnimationHelper.shakeView(text_username_or_email)
text_username_or_email.requestFocus()
override fun showFormView() {
text_username_or_email.setVisible(true)
text_password.setVisible(true)
}
override fun alertWrongPassword() {
AnimationHelper.vibrateSmartPhone(appContext)
AnimationHelper.shakeView(text_password)
text_password.requestFocus()
override fun hideFormView() {
text_username_or_email.setVisible(false)
text_password.setVisible(false)
}
override fun showLoading() {
enableUserInput(false)
view_loading.setVisible(true)
override fun setupLoginButtonListener() {
button_log_in.setOnClickListener {
presenter.authenticate(text_username_or_email.textContent, text_password.textContent)
}
}
override fun hideLoading() {
view_loading.setVisible(false)
enableUserInput(true)
override fun enableUserInput() {
button_log_in.isEnabled = true
text_username_or_email.isEnabled = true
text_password.isEnabled = true
}
override fun showMessage(resId: Int) = showToast(resId)
override fun showMessage(message: String) = showToast(message)
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showNoInternetConnection() = showMessage(getString(R.string.msg_no_internet_connection))
override fun disableUserInput() {
button_log_in.isEnabled = false
text_username_or_email.isEnabled = false
text_password.isEnabled = false
}
override fun showCasButton() {
button_cas.setVisible(true)
}
private fun tintEditTextDrawableStart() {
activity?.apply {
val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_assignment_ind_black_24dp, this)
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_24dp, this)
override fun hideCasButton() {
button_cas.setVisible(false)
}
val drawables = arrayOf(personDrawable, lockDrawable)
DrawableHelper.wrapDrawables(drawables)
DrawableHelper.tintDrawables(drawables, this, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawables(arrayOf(text_username_or_email, text_password), drawables)
override fun setupCasButtonListener(casUrl: String, casToken: String) {
button_cas.setOnClickListener {
startActivityForResult(context?.webViewIntent(casUrl, casToken), REQUEST_CODE_FOR_CAS)
activity?.overridePendingTransition(R.anim.slide_up, R.anim.hold)
}
}
private fun showLoginButton(value: Boolean) {
button_log_in.setVisible(value)
override fun showSignUpView() {
text_new_to_rocket_chat.setVisible(true)
}
private fun setupSignUpListener() {
override fun setupSignUpView() {
val signUp = getString(R.string.title_sign_up)
val newToRocketChat = String.format(getString(R.string.msg_new_user), signUp)
......@@ -209,31 +169,128 @@ class LoginFragment : Fragment(), LoginView {
TextHelper.addLink(text_new_to_rocket_chat, arrayOf(signUp), arrayOf(signUpListener))
}
private fun enableUserInput(value: Boolean) {
button_log_in.isEnabled = value
text_username_or_email.isEnabled = value
text_password.isEnabled = value
if (!isEditTextEmpty()) {
showSignUpView(value)
showOauthView(value)
override fun hideSignUpView() {
text_new_to_rocket_chat.setVisible(false)
}
override fun showOauthView() {
showThreeSocialAccountsMethods()
social_accounts_container.setVisible(true)
}
override fun hideOauthView() {
social_accounts_container.setVisible(false)
button_fab.setVisible(false)
}
override fun showLoginButton() {
button_log_in.setVisible(true)
}
override fun hideLoginButton() {
button_log_in.setVisible(false)
}
override fun enableLoginByFacebook() {
button_facebook.isEnabled = true
}
override fun enableLoginByGithub() {
button_github.isEnabled = true
}
override fun enableLoginByGoogle() {
button_google.isEnabled = true
}
override fun enableLoginByLinkedin() {
button_linkedin.isEnabled = true
}
override fun enableLoginByMeteor() {
button_meteor.isEnabled = true
}
override fun enableLoginByTwitter() {
button_twitter.isEnabled = true
}
override fun enableLoginByGitlab() {
button_gitlab.isEnabled = true
}
override fun setupFabListener() {
button_fab.setVisible(true)
button_fab.setOnClickListener({
button_fab.hide()
showRemainingSocialAccountsView()
scrollToBottom()
})
}
override fun setupGlobalListener() {
// We need to setup the layout to hide and show the oauth interface when the soft keyboard is shown
// (means that the user touched the text_username_or_email or text_password EditText to fill that respective fields).
if (!isGlobalLayoutListenerSetUp) {
scroll_view.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
isGlobalLayoutListenerSetUp = true
}
}
// Returns true if *all* EditTexts are empty.
private fun isEditTextEmpty(): Boolean = text_username_or_email.textContent.isBlank() && text_password.textContent.isEmpty()
override fun alertWrongUsernameOrEmail() {
vibrateSmartPhone()
text_username_or_email.shake()
text_username_or_email.requestFocus()
}
override fun alertWrongPassword() {
vibrateSmartPhone()
text_password.shake()
text_password.requestFocus()
}
private fun showRemainingSocialAccountsView() {
social_accounts_container.postDelayed({
for (i in 0..social_accounts_container.childCount) {
val view = social_accounts_container.getChildAt(i) as? ImageButton ?: continue
if (view.isEnabled) view.visibility = View.VISIBLE
}
(0..social_accounts_container.childCount)
.mapNotNull { social_accounts_container.getChildAt(it) as? ImageButton }
.filter { it.isEnabled }
.forEach { it.visibility = View.VISIBLE }
}, 1000)
}
// Scrolling to the bottom of the screen.
private fun scrollToBottom() {
scroll_view.postDelayed({
scroll_view.fullScroll(ScrollView.FOCUS_DOWN)
}, 1250)
}
}
private fun areLoginOptionsNeeded() {
if (!isEditTextEmpty() || KeyboardHelper.isSoftKeyboardShown(scroll_view.rootView)) {
hideSignUpView()
hideOauthView()
showLoginButton()
} else {
showSignUpView()
showOauthView()
hideLoginButton()
}
}
// Returns true if *all* EditTexts are empty.
private fun isEditTextEmpty(): Boolean {
return text_username_or_email.textContent.isBlank() && text_password.textContent.isEmpty()
}
private fun showThreeSocialAccountsMethods() {
var count = 0
for (i in 0..social_accounts_container.childCount) {
val view = social_accounts_container.getChildAt(i) as? ImageButton ?: continue
if (view.isEnabled && count < 3) {
view.visibility = View.VISIBLE
count++
}
}
}
}
\ No newline at end of file
......@@ -9,23 +9,24 @@ import chat.rocket.android.authentication.twofactor.ui.TwoFAFragment
import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.main.ui.MainActivity
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) {
fun toLogin() {
activity.addFragmentBackStack("loginFragment", R.id.fragment_container) {
activity.addFragmentBackStack("LoginFragment", R.id.fragment_container) {
LoginFragment.newInstance()
}
}
fun toTwoFA(username: String, password: String) {
activity.addFragmentBackStack("twoFAFragment", R.id.fragment_container) {
activity.addFragmentBackStack("TwoFAFragment", R.id.fragment_container) {
TwoFAFragment.newInstance(username, password)
}
}
fun toSignUp() {
activity.addFragmentBackStack("signupFragment", R.id.fragment_container) {
activity.addFragmentBackStack("SignupFragment", R.id.fragment_container) {
SignupFragment.newInstance()
}
}
......@@ -36,14 +37,11 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity, int
}
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 toServerScreen() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
}
\ No newline at end of file
......@@ -4,8 +4,9 @@ import chat.rocket.android.authentication.server.ui.ServerFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module abstract class ServerFragmentProvider {
@Module
abstract class ServerFragmentProvider {
@ContributesAndroidInjector(modules = [(ServerFragmentModule::class)])
@ContributesAndroidInjector(modules = [ServerFragmentModule::class])
abstract fun provideServerFragment(): ServerFragment
}
\ No newline at end of file
......@@ -4,66 +4,39 @@ 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.*
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.server.domain.RefreshSettingsInteractor
import chat.rocket.android.server.domain.SaveCurrentServerInteractor
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.settings
import java.security.InvalidParameterException
import javax.inject.Inject
class ServerPresenter @Inject constructor(private val view: ServerView,
private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator,
private val serverInteractor: SaveCurrentServerInteractor,
private val settingsInteractor: SaveSettingsInteractor,
private val factory: RocketChatClientFactory) {
private lateinit var client: RocketChatClient
private var settingsFilter = arrayOf(SITE_URL, SITE_NAME, FAVICON_512, USE_REALNAME, ALLOW_ROOM_NAME_SPECIAL_CHARS, FAVORITE_ROOMS,
ACCOUNT_LOGIN_FORM, ACCOUNT_GOOGLE, ACCOUNT_FACEBOOK, ACCOUNT_GITHUB, ACCOUNT_GITLAB, ACCOUNT_LINKEDIN, ACCOUNT_METEOR,
ACCOUNT_TWITTER, ACCOUNT_WORDPRESS, LDAP_ENABLE, ACCOUNT_REGISTRATION, STORAGE_TYPE, HIDE_USER_JOIN, HIDE_USER_LEAVE, HIDE_TYPE_AU,
HIDE_MUTE_UNMUTE, HIDE_TYPE_RU, ACCOUNT_CUSTOM_FIELDS, ALLOW_MESSAGE_DELETING, ALLOW_MESSAGE_EDITING, ALLOW_MESSAGE_PINNING,
SHOW_DELETED_STATUS, SHOW_EDITED_STATUS)
private val refreshSettingsInteractor: RefreshSettingsInteractor) {
fun connect(server: String) {
if (!UrlHelper.isValidUrl(server)) {
view.showInvalidServerUrl()
view.showInvalidServerUrlMessage()
} else {
try {
client = factory.create(server)
} catch (exception: InvalidParameterException) {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
return
}
client.let { rocketChatClient ->
launchUI(strategy) {
if (NetworkHelper.hasInternetAccess()) {
view.showLoading()
try {
val settings = rocketChatClient.settings(*settingsFilter)
settingsInteractor.save(server, settings)
serverInteractor.save(server)
navigator.toLogin()
} catch (exception: Exception) {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
launchUI(strategy) {
if (NetworkHelper.hasInternetAccess()) {
view.showLoading()
try {
refreshSettingsInteractor.refresh(server)
serverInteractor.save(server)
navigator.toLogin()
} catch (ex: Exception) {
ex.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} else {
view.showNoInternetConnection()
} finally {
view.hideLoading()
}
} else {
view.showNoInternetConnection()
}
}
}
......
......@@ -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
......@@ -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)
......
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()
}
......@@ -114,15 +110,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 +147,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
......
......@@ -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,10 +62,8 @@ 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))
......
package chat.rocket.android.chatroom.adapter
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,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<AudioAttachmentViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
image_attachment.setVisible(false)
audio_video_attachment.setVisible(true)
setupActionMenu(attachment_container)
setupActionMenu(audio_video_attachment)
}
}
override fun bindViews(data: AudioAttachmentViewModel) {
with(itemView) {
file_name.text = data.attachmentTitle
audio_video_attachment.setOnClickListener { view ->
data.attachmentUrl.let { url ->
PlayerActivity.play(view.context, url)
}
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.support.annotation.IntDef
const val PEOPLE = 0L
const val ROOMS = 1L
@Retention(AnnotationRetention.SOURCE)
@IntDef(value = [PEOPLE, ROOMS])
annotation class AutoCompleteType
package chat.rocket.android.chatroom.adapter
import android.support.v7.widget.RecyclerView
import android.view.MenuItem
import android.view.View
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,
var reactionListener: EmojiReactionListener? = null
) : RecyclerView.ViewHolder(itemView),
MenuItem.OnMenuItemClickListener {
var data: T? = null
init {
setupActionMenu(itemView)
}
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)
interface ActionsListener {
fun isActionsEnabled(): Boolean
fun onActionSelected(item: MenuItem, message: Message)
}
val longClickListener = { view: View ->
if (data?.message?.isSystemMessage() == false) {
val menuItems = view.context.inflate(R.menu.message_actions).toList()
menuItems.find { it.itemId == R.id.action_menu_msg_pin_unpin }?.apply {
val isPinned = data?.message?.pinned ?: false
setTitle(if (isPinned) R.string.action_msg_unpin else R.string.action_msg_pin)
isChecked = isPinned
}
val adapter = ActionListAdapter(menuItems, this@BaseViewHolder)
BottomSheetMenu(adapter).show(view.context)
}
true
}
internal fun setupActionMenu(view: View) {
if (listener.isActionsEnabled()) {
view.setOnLongClickListener(longClickListener)
}
}
override fun onMenuItemClick(item: MenuItem): Boolean {
data?.let {
listener.onActionSelected(item, it.message)
}
return true
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.support.v7.widget.RecyclerView
import android.view.MenuItem
import android.view.ViewGroup
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 reactionListener: EmojiReactionListener? = null
) : RecyclerView.Adapter<BaseViewHolder<*>>() {
private val dataSet = ArrayList<BaseViewModel<*>>()
init {
setHasStableIds(true)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<*> {
return when (viewType.toViewType()) {
BaseViewModel.ViewType.MESSAGE -> {
val view = parent.inflate(R.layout.item_message)
MessageViewHolder(view, actionsListener, reactionListener)
}
BaseViewModel.ViewType.IMAGE_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment)
ImageAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseViewModel.ViewType.AUDIO_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment)
AudioAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseViewModel.ViewType.VIDEO_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment)
VideoAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseViewModel.ViewType.URL_PREVIEW -> {
val view = parent.inflate(R.layout.message_url_preview)
UrlPreviewViewHolder(view, actionsListener, reactionListener)
}
else -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
}
}
}
override fun getItemViewType(position: Int): Int {
return dataSet[position].viewType
}
override fun getItemCount(): Int {
return dataSet.size
}
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)
}
}
override fun getItemId(position: Int): Long {
val model = dataSet[position]
return when (model) {
is MessageViewModel -> model.messageId.hashCode().toLong()
is BaseFileAttachmentViewModel -> model.id
else -> return position.toLong()
}
}
fun appendData(dataSet: List<BaseViewModel<*>>) {
val previousDataSetSize = this.dataSet.size
this.dataSet.addAll(dataSet)
notifyItemChanged(previousDataSetSize, dataSet.size)
}
fun prependData(dataSet: List<BaseViewModel<*>>) {
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<*>) {
var index = dataSet.indexOfLast { it.messageId == message.messageId }
val indexOfFirst = dataSet.indexOfFirst { it.messageId == message.messageId }
Timber.d("index: $index")
if (index > -1) {
dataSet[index] = message
notifyItemChanged(index)
while (dataSet[index].nextDownStreamMessage != null) {
dataSet[index].nextDownStreamMessage!!.reactions = message.reactions
notifyItemChanged(--index)
}
// Delete message only if current is a system message update, i.e.: Message Removed
if (message.message.isSystemMessage() && indexOfFirst > -1 && indexOfFirst != index) {
dataSet.removeAt(indexOfFirst)
notifyItemRemoved(indexOfFirst)
}
}
}
fun removeItem(messageId: String) {
val index = dataSet.indexOfFirst { it.messageId == messageId }
if (index > -1) {
val oldSize = dataSet.size
val newSet = dataSet.filterNot { it.messageId == messageId }
dataSet.clear()
dataSet.addAll(newSet)
val newSize = dataSet.size
notifyItemRangeRemoved(index, oldSize - newSize)
}
}
val actionsListener = object : BaseViewHolder.ActionsListener {
override fun isActionsEnabled(): Boolean = enableActions
override fun onActionSelected(item: MenuItem, message: Message) {
message.apply {
when (item.itemId) {
R.id.action_menu_msg_delete -> presenter?.deleteMessage(roomId, id)
R.id.action_menu_msg_quote -> presenter?.citeMessage(roomType, roomName, id, false)
R.id.action_menu_msg_reply -> presenter?.citeMessage(roomType, roomName, id, true)
R.id.action_menu_msg_copy -> presenter?.copyMessage(id)
R.id.action_menu_msg_edit -> presenter?.editMessage(roomId, id, message.message)
R.id.action_menu_msg_pin_unpin -> {
with(item) {
if (!isChecked) {
presenter?.pinMessage(id)
} else {
presenter?.unpinMessage(id)
}
}
}
R.id.action_menu_msg_react -> presenter?.showReactions(id)
else -> TODO("Not implemented")
}
}
}
}
}
\ 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.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 = UNLIMITED_RESULT_COUNT) {
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
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,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<ImageAttachmentViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(attachment_container)
setupActionMenu(image_attachment)
}
}
override fun bindViews(data: ImageAttachmentViewModel) {
with(itemView) {
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()
}
}
}
}
\ 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.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
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,
reactionListener: EmojiReactionListener? = null
) : BaseViewHolder<MessageViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
text_content.movementMethod = LinkMovementMethod()
setupActionMenu(text_content)
}
}
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
image_avatar.setImageURI(data.avatar)
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import DrawableHelper
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 chat.rocket.common.model.UserStatus
import com.facebook.drawee.view.SimpleDraweeView
class PeopleSuggestionsAdapter : SuggestionsAdapter<PeopleSuggestionViewHolder>("@") {
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()) {
avatar.setVisible(false)
} else {
avatar.setVisible(true)
avatar.setImageURI(item.imageUri)
}
val status = item.status ?: UserStatus.Offline()
val statusDrawable = DrawableHelper.getUserStatusDrawable(status, itemView.context)
statusView.setImageDrawable(statusDrawable)
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
package chat.rocket.android.chatroom.adapter
import android.content.Intent
import android.net.Uri
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,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<UrlPreviewViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(url_preview_layout)
}
}
override fun bindViews(data: UrlPreviewViewModel) {
with(itemView) {
if (data.thumbUrl.isNullOrEmpty()) {
image_preview.setVisible(false)
} else {
image_preview.setImageURI(data.thumbUrl)
image_preview.setVisible(true)
}
text_host.content = data.hostname
text_title.content = data.title
text_description.content = data.description ?: ""
url_preview_layout.setOnClickListener { view ->
view.context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(data.rawData.url)))
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
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,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<VideoAttachmentViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
image_attachment.setVisible(false)
audio_video_attachment.setVisible(true)
setupActionMenu(attachment_container)
setupActionMenu(audio_video_attachment)
}
}
override fun bindViews(data: VideoAttachmentViewModel) {
with(itemView) {
file_name.text = data.attachmentTitle
audio_video_attachment.setOnClickListener { view ->
data.attachmentUrl.let { url ->
PlayerActivity.play(view.context, url)
}
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.chatroom.presentation.ChatRoomNavigator
import chat.rocket.android.chatroom.presentation.ChatRoomView
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.chatroom.ui.ChatRoomFragment
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment
......@@ -13,6 +15,9 @@ import kotlinx.coroutines.experimental.Job
@PerFragment
class ChatRoomFragmentModule {
@Provides
fun provideChatRoomNavigator(activity: ChatRoomActivity) = ChatRoomNavigator(activity)
@Provides
fun chatRoomView(frag: ChatRoomFragment): ChatRoomView {
return frag
......
......@@ -24,7 +24,7 @@ class PinnedMessagesFragmentModule {
}
@Provides
fun providePinnedMessageView(frag: PinnedMessagesFragment): PinnedMessagesView {
fun providePinnedMessagesView(frag: PinnedMessagesFragment): PinnedMessagesView {
return frag
}
}
\ No newline at end of file
......@@ -2,11 +2,7 @@ package chat.rocket.android.chatroom.domain
import android.content.Context
import android.net.Uri
import chat.rocket.android.util.extensions.getFileName
import chat.rocket.android.util.extensions.getMimeType
import chat.rocket.android.util.extensions.getRealPathFromURI
import okio.Okio
import java.io.File
import chat.rocket.android.util.extensions.*
import javax.inject.Inject
......@@ -28,26 +24,7 @@ class UriInteractor @Inject constructor(private val context: Context) {
*/
fun getRealPath(uri: Uri): String? = uri.getRealPathFromURI(context)
/**
* Save the contents of an [Uri] to a temp file named after uri.getFileName()
*/
fun tempFile(uri: Uri): File? {
try {
val outputDir = context.cacheDir // context being the Activity pointer
val outputFile = File(outputDir, uri.getFileName(context))
val from = context.contentResolver.openInputStream(uri)
Okio.source(from).use { a ->
Okio.buffer(Okio.sink(outputFile)).use{ b ->
b.writeAll(a)
b.close()
}
a.close()
}
return outputFile
} catch (ex: Exception) {
ex.printStackTrace()
return null
}
}
fun getFileSize(uri: Uri) = uri.getFileSize(context)
fun getInputStream(uri: Uri) = uri.getInputStream(context)
}
\ No newline at end of file
package chat.rocket.android.chatroom.presentation
import chat.rocket.android.R
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.members.ui.newInstance
import chat.rocket.android.util.extensions.addFragmentBackStack
class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
fun toMembersList(chatRoomId: String, chatRoomType: String) {
activity.addFragmentBackStack("MembersFragment", R.id.fragment_container) {
newInstance(chatRoomId, chatRoomType)
}
}
}
\ No newline at end of file
......@@ -2,61 +2,83 @@ package chat.rocket.android.chatroom.presentation
import android.net.Uri
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.AutoCompleteType
import chat.rocket.android.chatroom.adapter.PEOPLE
import chat.rocket.android.chatroom.adapter.ROOMS
import chat.rocket.android.chatroom.domain.UriInteractor
import chat.rocket.android.chatroom.viewmodel.MessageViewModelMapper
import chat.rocket.android.chatroom.viewmodel.ViewModelMapper
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.lifecycle.CancelStrategy
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.state
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.UserStatus
import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.realtime.State
import chat.rocket.core.internal.realtime.connect
import chat.rocket.core.internal.realtime.subscribeRoomMessages
import chat.rocket.core.internal.realtime.unsubscibre
import chat.rocket.core.internal.rest.*
import chat.rocket.core.model.Command
import chat.rocket.core.model.Message
import chat.rocket.core.model.Value
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.launch
import org.threeten.bp.Instant
import timber.log.Timber
import java.io.File
import javax.inject.Inject
class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
private val navigator: ChatRoomNavigator,
private val strategy: CancelStrategy,
getSettingsInteractor: GetSettingsInteractor,
private val serverInteractor: GetCurrentServerInteractor,
private val getChatRoomsInteractor: GetChatRoomsInteractor,
private val permissions: GetPermissionsInteractor,
private val uriInteractor: UriInteractor,
private val messagesRepository: MessagesRepository,
factory: RocketChatClientFactory,
private val mapper: MessageViewModelMapper) {
private val client = factory.create(serverInteractor.get()!!)
private var subId: String? = null
private val usersRepository: UsersRepository,
private val roomsRepository: RoomRepository,
private val localRepository: LocalRepository,
factory: ConnectionManagerFactory,
private val mapper: ViewModelMapper) {
private val currentServer = serverInteractor.get()!!
private val manager = factory.create(currentServer)
private val client = manager.client
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!!
private val messagesChannel = Channel<Message>()
private var chatRoomId: String? = null
private var chatRoomType: String? = null
private val stateChannel = Channel<State>()
private var lastState = manager.state
fun loadMessages(chatRoomId: String, chatRoomType: String, offset: Long = 0) {
this.chatRoomId = chatRoomId
this.chatRoomType = chatRoomType
launchUI(strategy) {
view.showLoading()
try {
val messages =
client.messages(chatRoomId, roomTypeOf(chatRoomType), offset, 30).result
client.messages(chatRoomId, roomTypeOf(chatRoomType), offset, 30).result
messagesRepository.saveAll(messages)
val messagesViewModels = mapper.mapToViewModelList(messages, settings)
val messagesViewModels = mapper.map(messages)
view.showMessages(messagesViewModels)
// Subscribe after getting the first page of messages from REST
if (offset == 0L) {
subscribeMessages(chatRoomId)
}
// TODO: For now we are marking the room as read if we can get the messages (I mean, no exception occurs)
// but should mark only when the user see the first unread message.
markRoomAsRead(chatRoomId)
subscribeMessages(chatRoomId)
} catch (ex: Exception) {
ex.printStackTrace()
ex.message?.let {
......@@ -67,58 +89,58 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
} finally {
view.hideLoading()
}
if (offset == 0L) {
subscribeState()
}
}
}
fun sendMessage(chatRoomId: String, text: String, messageId: String?) {
launchUI(strategy) {
view.disableMessageInput()
view.disableSendMessageButton()
try {
// ignore message for now, will receive it on the stream
val message = if (messageId == null) {
client.sendMessage(chatRoomId, text)
} else {
client.updateMessage(chatRoomId, messageId, text)
}
// ignore message for now, will receive it on the stream
view.enableMessageInput(clear = true)
view.clearMessageComposition()
} catch (ex: Exception) {
ex.printStackTrace()
ex.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
view.enableMessageInput()
} finally {
view.enableSendMessageButton()
}
}
}
fun selectFile() {
view.showFileSelection(settings.uploadMimeTypeFilter())
}
fun uploadFile(roomId: String, uri: Uri, msg: String) {
launchUI(strategy) {
view.showLoading()
var tempFile: File? = null
try {
val fileName = async { uriInteractor.getFileName(uri) }.await()
val mimeType = async { uriInteractor.getMimeType(uri) }.await()
/* FIXME - this is a workaround for uploading files with the SDK
*
* https://developer.android.com/guide/topics/providers/document-provider.html
*
* We need to use contentResolver.openInputStream(uri) to open this file.
* Since the SDK is not Android specific we cannot pass the Uri and let the
* SDK handle the file.
*
* As a temporary workaround we are saving the contents to a temp file.
*
* A proper solution is to implement some interface to open the InputStream
* and use a RequestBody based on https://github.com/square/okhttp/issues/3585
*/
tempFile = async { uriInteractor.tempFile(uri) }.await()
if (fileName == null || tempFile == null) {
view.showInvalidFileMessage()
} else {
client.uploadFile(roomId, tempFile, mimeType, msg, fileName)
val fileSize = async { uriInteractor.getFileSize(uri) }.await()
val maxFileSize = settings.uploadMaxFileSize()
when {
fileName.isNullOrEmpty() -> view.showInvalidFileMessage()
fileSize > maxFileSize -> view.showInvalidFileSize(fileSize, maxFileSize)
else -> {
Timber.d("Uploading to $roomId: $fileName - $mimeType")
client.uploadFile(roomId, fileName!!, mimeType, msg, description = fileName) {
uriInteractor.getInputStream(uri)
}
}
}
} catch (ex: RocketChatException) {
Timber.d(ex)
......@@ -128,63 +150,91 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
view.showGenericErrorMessage()
}
} finally {
tempFile?.delete()
view.hideLoading()
}
}
}
fun subscribeMessages(roomId: String) {
client.addStateChannel(stateChannel)
launch(CommonPool + strategy.jobs) {
for (status in stateChannel) {
Timber.d("Changing status to: $status")
when (status) {
State.Authenticating -> Timber.d("Authenticating")
State.Connected -> {
Timber.d("Connected")
subId = client.subscribeRoomMessages(roomId) {
Timber.d("subscribe messages for $roomId: $it")
}
}
}
private fun markRoomAsRead(roomId: String) {
launchUI(strategy) {
try {
client.markAsRead(roomId)
} catch (ex: RocketChatException) {
view.showMessage(ex.message!!) // TODO Remove.
Timber.e(ex) // FIXME: Right now we are only catching the exception with Timber.
}
Timber.d("Done on statusChannel")
}
}
private fun subscribeState() {
Timber.d("Subscribing to Status changes")
lastState = manager.state
manager.addStatusChannel(stateChannel)
launch(CommonPool + strategy.jobs) {
for (state in stateChannel) {
Timber.d("Got new state: $state - last: $lastState")
if (state != lastState) {
launch(UI) {
view.showConnectionState(state)
}
when (client.state) {
State.Connected -> {
Timber.d("Already connected")
subId = client.subscribeRoomMessages(roomId) {
Timber.d("subscribe messages for $roomId: $it")
if (state is State.Connected) {
loadMissingMessages()
}
}
lastState = state
}
else -> client.connect()
}
}
launchUI(strategy) {
listenMessages(roomId)
}
private fun subscribeMessages(roomId: String) {
manager.subscribeRoomMessages(roomId, messagesChannel)
// TODO - when we have a proper service, we won't need to take care of connection, just
// subscribe and listen...
/*launchUI(strategy) {
subId = client.subscribeRoomMessages(roomId) {
Timber.d("subscribe messages for $roomId: $it")
launch(CommonPool + strategy.jobs) {
for (message in messagesChannel) {
Timber.d("New message for room ${message.roomId}")
updateMessage(message)
}
listenMessages(roomId)
}*/
}
}
fun unsubscribeMessages() {
launch(CommonPool) {
client.removeStateChannel(stateChannel)
subId?.let { subscriptionId ->
client.unsubscibre(subscriptionId)
private fun loadMissingMessages() {
launch(parent = strategy.jobs) {
if (chatRoomId != null && chatRoomType != null) {
val roomType = roomTypeOf(chatRoomType!!)
messagesRepository.getByRoomId(chatRoomId!!)
.sortedByDescending { it.timestamp }.firstOrNull()?.let { lastMessage ->
val instant = Instant.ofEpochMilli(lastMessage.timestamp)
val messages = client.history(chatRoomId!!, roomType, count = 50,
oldest = instant.toString())
Timber.d("History: $messages")
if (messages.result.isNotEmpty()) {
val models = mapper.map(messages.result)
messagesRepository.saveAll(messages.result)
launchUI(strategy) {
view.showNewMessage(models)
}
if (messages.result.size == 50) {
// we loade at least count messages, try one more to fetch more messages
loadMissingMessages()
}
}
}
}
}
}
fun unsubscribeMessages(chatRoomId: String) {
manager.removeStatusChannel(stateChannel)
manager.unsubscribeRoomMessages(chatRoomId)
// All messages during the subscribed period are assumed to be read,
// and lastSeen is updated as the time when the user leaves the room
markRoomAsRead(chatRoomId)
}
/**
* Delete the message with the given id.
*
......@@ -239,9 +289,9 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
is RoomType.Custom -> "custom" //TODO: put appropriate callback string here.
}
view.showReplyingAction(
user,
"[ ](${serverUrl}/${room}/${roomName}?msg=${id}) ${mention} ",
m.message
username = user,
replyMarkdown = "[ ]($serverUrl/$room/$roomName?msg=$id) $mention ",
quotedMessage = m.message
)
}
}
......@@ -309,20 +359,187 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
}
}
private suspend fun listenMessages(roomId: String) {
launch(CommonPool + strategy.jobs) {
for (message in client.messagesChannel) {
if (message.roomId != roomId) {
Timber.d("Ignoring message for room ${message.roomId}, expecting $roomId")
fun loadActiveMembers(chatRoomId: String, chatRoomType: String, offset: Long = 0, filterSelfOut: Boolean = false) {
launchUI(strategy) {
try {
val members = client.getMembers(chatRoomId, roomTypeOf(chatRoomType), offset, 50).result
usersRepository.saveAll(members)
val self = localRepository.get(LocalRepository.USERNAME_KEY)
// Take at most the 100 most recent messages distinguished by user. Can return less.
val recentMessages = messagesRepository.getRecentMessages(chatRoomId, 100)
.filterNot { filterSelfOut && it.sender?.username == self }
val activeUsers = mutableListOf<PeopleSuggestionViewModel>()
recentMessages.forEach {
val sender = it.sender!!
val username = sender.username ?: ""
val name = sender.name ?: ""
val avatarUrl = UrlHelper.getAvatarUrl(currentServer, username)
val found = members.firstOrNull { member -> member.username == username }
val status = if (found != null) found.status else UserStatus.Offline()
val searchList = mutableListOf(username, name)
activeUsers.add(PeopleSuggestionViewModel(avatarUrl, username, username, name, status,
true, searchList))
}
updateMessage(message)
// Filter out from members list the active users.
val others = members.filterNot { member ->
activeUsers.firstOrNull {
it.username == member.username
} != null
}
// Add found room members who're not active enough and add them in without pinning.
activeUsers.addAll(others.map {
val username = it.username ?: ""
val name = it.name ?: ""
val avatarUrl = UrlHelper.getAvatarUrl(currentServer, username)
val searchList = mutableListOf(username, name)
PeopleSuggestionViewModel(avatarUrl, username, username, name, it.status, true, searchList)
})
view.populatePeopleSuggestions(activeUsers)
} catch (e: RocketChatException) {
Timber.e(e)
}
}
}
fun spotlight(query: String, @AutoCompleteType type: Long, filterSelfOut: Boolean = false) {
launchUI(strategy) {
try {
val (users, rooms) = client.spotlight(query)
when (type) {
PEOPLE -> {
if (users.isNotEmpty()) {
usersRepository.saveAll(users)
}
val self = localRepository.get(LocalRepository.USERNAME_KEY)
view.populatePeopleSuggestions(users.map {
val username = it.username ?: ""
val name = it.name ?: ""
val searchList = mutableListOf(username, name)
it.emails?.forEach { email -> searchList.add(email.address) }
PeopleSuggestionViewModel(UrlHelper.getAvatarUrl(currentServer, username),
username, username, name, it.status, false, searchList)
}.filterNot { filterSelfOut && self != null && self == it.text })
}
ROOMS -> {
if (rooms.isNotEmpty()) {
roomsRepository.saveAll(rooms)
}
view.populateRoomSuggestions(rooms.map {
val fullName = it.fullName ?: ""
val name = it.name ?: ""
val searchList = mutableListOf(fullName, name)
ChatRoomSuggestionViewModel(name, fullName, name, searchList)
})
}
}
} catch (e: RocketChatException) {
Timber.e(e)
}
}
}
fun toMembersList(chatRoomId: String, chatRoomType: String) = navigator.toMembersList(chatRoomId, chatRoomType)
fun loadChatRooms() {
launchUI(strategy) {
try {
val chatRooms = getChatRoomsInteractor.get(currentServer)
.filterNot {
it.type is RoomType.DirectMessage || it.type is RoomType.Livechat
}
.map { chatRoom ->
val name = chatRoom.name
val fullName = chatRoom.fullName ?: ""
ChatRoomSuggestionViewModel(
text = name,
name = name,
fullName = fullName,
searchList = listOf(name, fullName)
)
}
view.populateRoomSuggestions(chatRooms)
} catch (e: RocketChatException) {
Timber.e(e)
}
}
}
fun joinChat(chatRoomId: String) {
launchUI(strategy) {
try {
client.joinChat(chatRoomId)
view.onJoined()
} catch (ex: RocketChatException) {
Timber.e(ex)
}
}
}
/**
* Send an emoji reaction to a message.
*/
fun react(messageId: String, emoji: String) {
launchUI(strategy) {
try {
client.toggleReaction(messageId, emoji.removeSurrounding(":"))
} catch (ex: RocketChatException) {
Timber.e(ex)
}
}
}
fun showReactions(messageId: String) {
view.showReactionsPopup(messageId)
}
fun loadCommands() {
launchUI(strategy) {
try {
//TODO: cache the commands
val commands = client.commands(0, 100).result
view.populateCommandSuggestions(commands.map {
println("${it.command} - ${it.description}")
CommandSuggestionViewModel(it.command, it.description ?: "", listOf(it.command))
})
} catch (ex: RocketChatException) {
Timber.e(ex)
}
}
}
fun runCommand(text: String, roomId: String) {
launchUI(strategy) {
try {
if (text.length == 1) {
// we have just the slash, post it anyway
sendMessage(roomId, text, null)
} else {
val command = text.split(" ")
val name = command[0].substring(1)
var params: String = ""
command.forEachIndexed { index, param ->
if (index > 0) {
params += "$param "
}
}
val result = client.runCommand(Command(name, params), roomId)
if (!result) {
// failed, command is not valid so post it
sendMessage(roomId, text, null)
}
}
} catch (ex: RocketChatException) {
Timber.e(ex)
// command is not valid, post it
sendMessage(roomId, text, null)
}
}
}
private fun updateMessage(streamedMessage: Message) {
launchUI(strategy) {
val viewModelStreamedMessage = mapper.mapToViewModel(streamedMessage, settings)
val viewModelStreamedMessage = mapper.map(streamedMessage)
val roomMessages = messagesRepository.getByRoomId(streamedMessage.roomId)
val index = roomMessages.indexOfFirst { msg -> msg.id == streamedMessage.id }
if (index > -1) {
......@@ -336,4 +553,4 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
}
}
}
}
\ No newline at end of file
}
package chat.rocket.android.chatroom.presentation
import android.net.Uri
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
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
interface ChatRoomView : LoadingView, MessageView {
......@@ -12,7 +16,7 @@ interface ChatRoomView : LoadingView, MessageView {
*
* @param dataSet The data set to show.
*/
fun showMessages(dataSet: List<MessageViewModel>)
fun showMessages(dataSet: List<BaseViewModel<*>>)
/**
* Send a message to a chat room.
......@@ -21,6 +25,11 @@ interface ChatRoomView : LoadingView, MessageView {
*/
fun sendMessage(text: String)
/**
* Perform file selection with the mime type [filter]
*/
fun showFileSelection(filter: Array<String>)
/**
* Uploads a file to a chat room.
*
......@@ -38,7 +47,7 @@ interface ChatRoomView : LoadingView, MessageView {
*
* @param message The (recent) message sent to a chat room.
*/
fun showNewMessage(message: MessageViewModel)
fun showNewMessage(message: List<BaseViewModel<*>>)
/**
* Dispatch to the recycler views adapter that we should remove a message.
......@@ -52,7 +61,7 @@ interface ChatRoomView : LoadingView, MessageView {
*
* @param index The index of the changed message
*/
fun dispatchUpdateMessage(index: Int, message: MessageViewModel)
fun dispatchUpdateMessage(index: Int, message: List<BaseViewModel<*>>)
/**
* Show reply status above the message composer.
......@@ -75,7 +84,38 @@ interface ChatRoomView : LoadingView, MessageView {
*/
fun showEditingAction(roomId: String, messageId: String, text: String)
fun disableMessageInput()
/**
* Disabling the send message button avoids the user tap this button multiple
* times to send a same message.
*/
fun disableSendMessageButton()
/**
* Enables the send message button.
*/
fun enableSendMessageButton()
fun enableMessageInput(clear: Boolean = false)
/**
* Clears the message composition.
*/
fun clearMessageComposition()
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
package chat.rocket.android.chatroom.presentation
import chat.rocket.android.chatroom.viewmodel.MessageViewModelMapper
import chat.rocket.android.chatroom.viewmodel.ViewModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetChatRoomsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
......@@ -11,6 +11,7 @@ import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.rest.getRoomPinnedMessages
import chat.rocket.core.model.Value
import chat.rocket.core.model.isSystemMessage
import timber.log.Timber
import javax.inject.Inject
......@@ -18,7 +19,7 @@ class PinnedMessagesPresenter @Inject constructor(private val view: PinnedMessag
private val strategy: CancelStrategy,
private val serverInteractor: GetCurrentServerInteractor,
private val roomsInteractor: GetChatRoomsInteractor,
private val mapper: MessageViewModelMapper,
private val mapper: ViewModelMapper,
factory: RocketChatClientFactory,
getSettingsInteractor: GetSettingsInteractor) {
......@@ -41,8 +42,7 @@ class PinnedMessagesPresenter @Inject constructor(private val view: PinnedMessag
val pinnedMessages =
client.getRoomPinnedMessages(roomId, room.type, pinnedMessagesListOffset)
pinnedMessagesListOffset = pinnedMessages.offset.toInt()
val messageList = mapper.mapToViewModelList(pinnedMessages.result, settings)
.filterNot { it.isSystemMessage }
val messageList = mapper.map(pinnedMessages.result.filterNot { it.isSystemMessage() })
view.showPinnedMessages(messageList)
view.hideLoading()
}.ifNull {
......
package chat.rocket.android.chatroom.presentation
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
......@@ -11,5 +11,5 @@ interface PinnedMessagesView : MessageView, LoadingView {
*
* @param pinnedMessages The list of pinned messages.
*/
fun showPinnedMessages(pinnedMessages: List<MessageViewModel>)
fun showPinnedMessages(pinnedMessages: List<BaseViewModel<*>>)
}
\ No newline at end of file
package chat.rocket.android.chatroom.ui
import DrawableHelper
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import chat.rocket.android.R
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.util.extensions.textContent
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.roomTypeOf
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
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)
}
}
......@@ -29,19 +43,31 @@ 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>
// TODO - workaround for now... We will move to a single activity
@Inject lateinit var serverInteractor: GetCurrentServerInteractor
@Inject lateinit var managerFactory: ConnectionManagerFactory
private lateinit var chatRoomId: String
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)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_chat_room)
// Workaround for when we are coming to the app via the recents app and the app was killed.
managerFactory.create(serverInteractor.get()!!).connect()
chatRoomId = intent.getStringExtra(INTENT_CHAT_ROOM_ID)
requireNotNull(chatRoomId) { "no chat_room_id provided in Intent extras" }
......@@ -54,28 +80,63 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
isChatRoomReadOnly = intent.getBooleanExtra(INTENT_IS_CHAT_ROOM_READ_ONLY, true)
requireNotNull(chatRoomType) { "no is_chat_room_read_only provided in Intent extras" }
setupToolbar(chatRoomName)
setupToolbar()
chatRoomLastSeen = intent.getLongExtra(INTENT_CHAT_ROOM_LAST_SEEN, -1)
addFragment("ChatRoomFragment", R.id.fragment_container) {
newInstance(chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly)
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)
}
}
}
override fun onBackPressed() = finishActivity()
override fun onBackPressed() {
finishActivity()
}
override fun supportFragmentInjector(): AndroidInjector<Fragment> {
return fragmentDispatchingAndroidInjector
}
private fun setupToolbar(chatRoomName: String) {
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)
}
toolbar.setNavigationOnClickListener {
finishActivity()
}
}
fun setupToolbarTitle(toolbarTitle: String) {
text_room_name.textContent = toolbarTitle
}
private fun finishActivity() {
super.onBackPressed()
overridePendingTransition(R.anim.close_enter, R.anim.close_exit)
......
package chat.rocket.android.chatroom.ui
import android.support.v7.widget.RecyclerView
import android.text.method.LinkMovementMethod
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.ui.bottomsheet.BottomSheetMenu
import chat.rocket.android.chatroom.ui.bottomsheet.adapter.ActionListAdapter
import chat.rocket.android.chatroom.viewmodel.AttachmentType
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.util.extensions.content
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible
import com.facebook.drawee.view.SimpleDraweeView
import com.stfalcon.frescoimageviewer.ImageViewer
import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_message.view.*
import kotlinx.android.synthetic.main.message_attachment.view.*
import ru.whalemare.sheetmenu.extension.inflate
import ru.whalemare.sheetmenu.extension.toList
class ChatRoomAdapter(private val roomType: String,
private val roomName: String,
private val presenter: ChatRoomPresenter) : RecyclerView.Adapter<ChatRoomAdapter.ViewHolder>() {
private val dataSet = ArrayList<MessageViewModel>()
init {
setHasStableIds(true)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(parent.inflate(R.layout.item_message), roomType, roomName, presenter)
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(dataSet[position])
override fun getItemCount(): Int = dataSet.size
override fun getItemViewType(position: Int): Int = position
override fun getItemId(position: Int): Long = dataSet[position].id.hashCode().toLong()
fun addDataSet(dataSet: List<MessageViewModel>) {
val previousDataSetSize = this.dataSet.size
this.dataSet.addAll(previousDataSetSize, dataSet)
notifyItemRangeInserted(previousDataSetSize, dataSet.size)
}
fun addItem(message: MessageViewModel) {
dataSet.add(0, message)
notifyItemInserted(0)
}
fun updateItem(message: MessageViewModel) {
val index = dataSet.indexOfFirst { it.id == message.id }
if (index > -1) {
dataSet[index] = message
notifyItemChanged(index)
}
}
fun removeItem(messageId: String) {
val index = dataSet.indexOfFirst { it.id == messageId }
if (index > -1) {
dataSet.removeAt(index)
notifyItemRemoved(index)
}
}
class ViewHolder(itemView: View,
val roomType: String,
val roomName: String,
val presenter: ChatRoomPresenter) : RecyclerView.ViewHolder(itemView), MenuItem.OnMenuItemClickListener {
private lateinit var messageViewModel: MessageViewModel
fun bind(message: MessageViewModel) = with(itemView) {
messageViewModel = message
image_avatar.setImageURI(message.avatarUri)
text_sender.text = message.senderName
text_message_time.content = message.time
text_content.content = message.content
text_content.movementMethod = LinkMovementMethod()
bindAttachment(message, message_attachment, image_attachment, audio_video_attachment, file_name)
text_content.setOnClickListener {
if (!message.isSystemMessage) {
val menuItems = it.context.inflate(R.menu.message_actions).toList()
menuItems.find { it.itemId == R.id.action_menu_msg_pin_unpin }?.apply {
val isPinned = message.isPinned
setTitle(if (isPinned) R.string.action_msg_unpin else R.string.action_msg_pin)
setChecked(isPinned)
}
val adapter = ActionListAdapter(menuItems, this@ViewHolder)
BottomSheetMenu(adapter).apply {
}.show(it.context)
}
}
}
override fun onMenuItemClick(item: MenuItem): Boolean {
messageViewModel.apply {
when (item.itemId) {
R.id.action_menu_msg_delete -> presenter.deleteMessage(roomId, id)
R.id.action_menu_msg_quote -> presenter.citeMessage(roomType, roomName, id, false)
R.id.action_menu_msg_reply -> presenter.citeMessage(roomType, roomName, id, true)
R.id.action_menu_msg_copy -> presenter.copyMessage(id)
R.id.action_menu_msg_edit -> presenter.editMessage(roomId, id, getOriginalMessage())
R.id.action_menu_msg_pin_unpin -> {
with(item) {
if (!isChecked) {
presenter.pinMessage(id)
} else {
presenter.unpinMessage(id)
}
}
}
else -> TODO("Not implemented")
}
}
return true
}
private fun bindAttachment(message: MessageViewModel,
attachment_container: View,
image_attachment: SimpleDraweeView,
audio_video_attachment: View,
file_name: TextView) {
with(message) {
if (attachmentUrl == null || attachmentType == null) {
attachment_container.setVisible(false)
return
}
var imageVisible = false
var videoVisible = false
attachment_container.setVisible(true)
when (message.attachmentType) {
is AttachmentType.Image -> {
imageVisible = true
image_attachment.setImageURI(message.attachmentUrl)
image_attachment.setOnClickListener { view ->
// TODO - implement a proper image viewer with a proper Transition
ImageViewer.Builder(view.context, listOf(message.attachmentUrl))
.setStartPosition(0)
.show()
}
}
is AttachmentType.Video,
is AttachmentType.Audio -> {
videoVisible = true
audio_video_attachment.setOnClickListener { view ->
message.attachmentUrl?.let { url ->
PlayerActivity.play(view.context, url)
}
}
}
}
image_attachment.setVisible(imageVisible)
audio_video_attachment.setVisible(videoVisible)
file_name.text = message.attachmentTitle
}
}
}
}
\ No newline at end of file
......@@ -8,31 +8,50 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.support.annotation.DrawableRes
import android.support.v4.app.Fragment
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.*
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.*
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.presentation.ChatRoomView
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
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.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.util.extensions.*
import chat.rocket.android.widget.emoji.*
import chat.rocket.core.internal.realtime.State
import dagger.android.support.AndroidSupportInjection
import io.reactivex.disposables.CompositeDisposable
import kotlinx.android.synthetic.main.fragment_chat_room.*
import kotlinx.android.synthetic.main.message_attachment_options.*
import kotlinx.android.synthetic.main.message_composer.*
import kotlinx.android.synthetic.main.message_list.*
import timber.log.Timber
import javax.inject.Inject
fun newInstance(chatRoomId: String, chatRoomName: String, chatRoomType: String, isChatRoomReadOnly: Boolean): Fragment {
fun newInstance(chatRoomId: String,
chatRoomName: String,
chatRoomType: String,
isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long,
isSubscribed: Boolean = true): Fragment {
return ChatRoomFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
putString(BUNDLE_CHAT_ROOM_NAME, chatRoomName)
putString(BUNDLE_CHAT_ROOM_TYPE, chatRoomType)
putBoolean(BUNDLE_IS_CHAT_ROOM_READ_ONLY, isChatRoomReadOnly)
putLong(BUNDLE_CHAT_ROOM_LAST_SEEN, chatRoomLastSeen)
putBoolean(BUNDLE_CHAT_ROOM_IS_SUBSCRIBED, isSubscribed)
}
}
}
......@@ -42,27 +61,33 @@ private const val BUNDLE_CHAT_ROOM_NAME = "chat_room_name"
private const val BUNDLE_CHAT_ROOM_TYPE = "chat_room_type"
private const val BUNDLE_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only"
private const val REQUEST_CODE_FOR_PERFORM_SAF = 42
private const val BUNDLE_CHAT_ROOM_LAST_SEEN = "chat_room_last_seen"
private const val BUNDLE_CHAT_ROOM_IS_SUBSCRIBED = "chat_room_is_subscribed"
class ChatRoomFragment : Fragment(), ChatRoomView {
class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiReactionListener {
@Inject lateinit var presenter: ChatRoomPresenter
@Inject lateinit var parser: MessageParser
private lateinit var adapter: ChatRoomAdapter
private lateinit var chatRoomId: String
private lateinit var chatRoomName: String
private lateinit var chatRoomType: String
private var isSubscribed: Boolean = true
private var isChatRoomReadOnly: Boolean = false
private lateinit var emojiKeyboardPopup: EmojiKeyboardPopup
private var chatRoomLastSeen: Long = -1
private lateinit var actionSnackbar: ActionSnackbar
private var citation: String? = null
private var editingMessageId: String? = null
private val compositeDisposable = CompositeDisposable()
private var playComposeMessageButtonsAnimation = true
// For reveal and unreveal anim.
private val hypotenuse by lazy { Math.hypot(root_layout.width.toDouble(), root_layout.height.toDouble()).toFloat() }
private val max by lazy { Math.max(layout_message_attachment_options.width.toDouble(), layout_message_attachment_options.height.toDouble()).toFloat() }
private val centerX by lazy { recycler_view.right }
private val centerY by lazy { recycler_view.bottom }
val handler = Handler()
private val handler = Handler()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
......@@ -74,6 +99,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
chatRoomName = bundle.getString(BUNDLE_CHAT_ROOM_NAME)
chatRoomType = bundle.getString(BUNDLE_CHAT_ROOM_TYPE)
isChatRoomReadOnly = bundle.getBoolean(BUNDLE_IS_CHAT_ROOM_READ_ONLY)
isSubscribed = bundle.getBoolean(BUNDLE_CHAT_ROOM_IS_SUBSCRIBED)
chatRoomLastSeen = bundle.getLong(BUNDLE_CHAT_ROOM_LAST_SEEN)
} else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
}
......@@ -84,14 +111,31 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar(chatRoomName)
presenter.loadMessages(chatRoomId, chatRoomType)
setupComposer()
presenter.loadChatRooms()
setupRecyclerView()
setupFab()
setupMessageComposer()
setupSuggestionsView()
setupActionSnackbar()
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
text_message.addTextChangedListener(EmojiKeyboardPopup.EmojiTextWatcher(text_message))
}
override fun onDestroyView() {
presenter.unsubscribeMessages()
presenter.unsubscribeMessages(chatRoomId)
handler.removeCallbacksAndMessages(null)
unsubscribeTextMessage()
// Hides the keyboard (if it's opened) before going to any view.
activity?.apply {
hideKeyboard()
}
super.onDestroyView()
}
......@@ -110,6 +154,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_members_list -> {
presenter.toMembersList(chatRoomId, chatRoomType)
}
R.id.action_pinned_messages -> {
val intent = Intent(activity, PinnedMessagesActivity::class.java).apply {
putExtra(BUNDLE_CHAT_ROOM_ID, chatRoomId)
......@@ -122,12 +169,35 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
return true
}
override fun showMessages(dataSet: List<MessageViewModel>) {
override fun showMessages(dataSet: List<BaseViewModel<*>>) {
// track the message sent immediately after the current message
var prevMessageViewModel: MessageViewModel? = null
// Loop over received messages to determine first unread
for (i in dataSet.indices) {
val msgModel = dataSet[i]
if (msgModel is MessageViewModel) {
val msg = msgModel.rawData
if (msg.timestamp < chatRoomLastSeen) {
// This message was sent before the last seen of the room. Hence, it was seen.
// if there is a message after (below) this, mark it firstUnread.
if (prevMessageViewModel != null) {
prevMessageViewModel.isFirstUnread = true
}
break
}
prevMessageViewModel = msgModel
}
}
activity?.apply {
if (recycler_view.adapter == null) {
adapter = ChatRoomAdapter(chatRoomType, chatRoomName, presenter)
adapter = ChatRoomAdapter(chatRoomType, chatRoomName, presenter,
reactionListener = this@ChatRoomFragment)
recycler_view.adapter = adapter
val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
linearLayoutManager.stackFromEnd = true
recycler_view.layoutManager = linearLayoutManager
recycler_view.itemAnimator = DefaultItemAnimator()
if (dataSet.size >= 30) {
......@@ -138,13 +208,24 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
})
}
}
adapter.addDataSet(dataSet)
val oldMessagesCount = adapter.itemCount
adapter.appendData(dataSet)
recycler_view.scrollToPosition(92)
if (oldMessagesCount == 0 && dataSet.isNotEmpty()) {
recycler_view.scrollToPosition(0)
}
presenter.loadActiveMembers(chatRoomId, chatRoomType, filterSelfOut = true)
}
}
override fun sendMessage(text: String) {
if (!text.isBlank()) {
presenter.sendMessage(chatRoomId, text, editingMessageId)
if (!text.startsWith("/")) {
presenter.sendMessage(chatRoomId, text, editingMessageId)
} else {
presenter.runCommand(text, chatRoomId)
}
}
}
......@@ -155,25 +236,33 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
override fun showInvalidFileMessage() = showMessage(getString(R.string.msg_invalid_file))
override fun showNewMessage(message: MessageViewModel) {
text_message.textContent = ""
adapter.addItem(message)
recycler_view.smoothScrollToPosition(0)
override fun showNewMessage(message: List<BaseViewModel<*>>) {
adapter.prependData(message)
recycler_view.scrollToPosition(0)
}
override fun disableMessageInput() {
override fun disableSendMessageButton() {
button_send.isEnabled = false
text_message.isEnabled = false
}
override fun enableMessageInput(clear: Boolean) {
override fun enableSendMessageButton() {
button_send.isEnabled = true
text_message.isEnabled = true
if (clear) text_message.textContent = ""
text_message.erase()
}
override fun dispatchUpdateMessage(index: Int, message: MessageViewModel) {
adapter.updateItem(message)
override fun clearMessageComposition() {
citation = null
editingMessageId = null
text_message.textContent = ""
actionSnackbar.dismiss()
}
override fun dispatchUpdateMessage(index: Int, message: List<BaseViewModel<*>>) {
adapter.updateItem(message.last())
if (message.size > 1) {
adapter.prependData(listOf(message.first()))
}
}
override fun dispatchDeleteMessage(msgId: String) {
......@@ -186,6 +275,12 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
actionSnackbar.title = username
actionSnackbar.text = quotedMessage
actionSnackbar.show()
KeyboardHelper.showSoftKeyboard(text_message)
if (!recycler_view.isAtBottom()) {
if (adapter.itemCount > 0) {
recycler_view.scrollToPosition(0)
}
}
}
}
......@@ -199,6 +294,18 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun populatePeopleSuggestions(members: List<PeopleSuggestionViewModel>) {
suggestions_view.addItems("@", members)
}
override fun populateRoomSuggestions(chatRooms: List<ChatRoomSuggestionViewModel>) {
suggestions_view.addItems("#", chatRooms)
}
override fun populateCommandSuggestions(commands: List<CommandSuggestionViewModel>) {
suggestions_view.addItems("/", commands)
}
override fun copyToClipboard(message: String) {
activity?.apply {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
......@@ -213,38 +320,159 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
actionSnackbar.show()
text_message.textContent = text
editingMessageId = messageId
KeyboardHelper.showSoftKeyboard(text_message)
}
}
override fun onEmojiAdded(emoji: Emoji) {
val cursorPosition = text_message.selectionStart
if (cursorPosition > -1) {
text_message.text.insert(cursorPosition, EmojiParser.parse(emoji.shortname))
text_message.setSelection(cursorPosition + emoji.unicode.length)
}
}
private fun setupComposer() {
override fun onNonEmojiKeyPressed(keyCode: Int) {
when (keyCode) {
KeyEvent.KEYCODE_BACK -> with(text_message) {
if (selectionStart > 0) text.delete(selectionStart - 1, selectionStart)
}
else -> throw IllegalArgumentException("pressed key not expected")
}
}
override fun onReactionTouched(messageId: String, emojiShortname: String) {
presenter.react(messageId, emojiShortname)
}
override fun onReactionAdded(messageId: String, emoji: Emoji) {
presenter.react(messageId, emoji.shortname)
}
override fun showReactionsPopup(messageId: String) {
context?.let {
val emojiPickerPopup = EmojiPickerPopup(it)
emojiPickerPopup.listener = object : EmojiListenerAdapter() {
override fun onEmojiAdded(emoji: Emoji) {
onReactionAdded(messageId, emoji)
}
}
emojiPickerPopup.show()
}
}
private fun setReactionButtonIcon(@DrawableRes drawableId: Int) {
button_add_reaction.setImageResource(drawableId)
button_add_reaction.setTag(drawableId)
}
override fun showFileSelection(filter: Array<String>) {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
intent.putExtra(Intent.EXTRA_MIME_TYPES, filter)
startActivityForResult(intent, REQUEST_CODE_FOR_PERFORM_SAF)
}
override fun showInvalidFileSize(fileSize: Int, maxFileSize: Int) {
showMessage(getString(R.string.max_file_size_exceeded, fileSize, maxFileSize))
}
override fun showConnectionState(state: State) {
activity?.apply {
connection_status_text.fadeIn()
handler.removeCallbacks(dismissStatus)
when (state) {
is State.Connected -> {
connection_status_text.text = getString(R.string.status_connected)
handler.postDelayed(dismissStatus, 2000)
}
is State.Disconnected -> connection_status_text.text = getString(R.string.status_disconnected)
is State.Connecting -> connection_status_text.text = getString(R.string.status_connecting)
is State.Authenticating -> connection_status_text.text = getString(R.string.status_authenticating)
is State.Disconnecting -> connection_status_text.text = getString(R.string.status_disconnecting)
is State.Waiting -> connection_status_text.text = getString(R.string.status_waiting, state.seconds)
}
}
}
override fun onJoined() {
input_container.setVisible(true)
button_join_chat.setVisible(false)
isSubscribed = true
setupMessageComposer()
}
private val dismissStatus = {
connection_status_text.fadeOut()
}
private fun setupRecyclerView() {
recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
Timber.i("Scrolling vertically: $dy")
if (!recyclerView.canScrollVertically(1)) {
button_fab.hide()
} else {
if (dy < 0 && !button_fab.isVisible()) {
button_fab.show()
}
}
}
})
}
private fun setupFab() {
button_fab.setOnClickListener {
recycler_view.scrollToPosition(0)
button_fab.hide()
}
}
private fun setupMessageComposer() {
if (isChatRoomReadOnly) {
text_room_is_read_only.setVisible(true)
input_container.setVisible(false)
} else if (!isSubscribed) {
input_container.setVisible(false)
button_join_chat.setVisible(true)
button_join_chat.setOnClickListener { presenter.joinChat(chatRoomId) }
} else {
var playAnimation = true
text_message.asObservable(0)
.subscribe({ t ->
if (t.isNotEmpty() && playAnimation) {
button_show_attachment_options.fadeInOrOut(1F, 0F, 120)
button_send.fadeInOrOut(0F, 1F, 120)
playAnimation = false
button_send.alpha = 0f
button_send.setVisible(false)
button_show_attachment_options.alpha = 1f
button_show_attachment_options.setVisible(true)
subscribeTextMessage()
emojiKeyboardPopup = EmojiKeyboardPopup(activity!!, activity!!.findViewById(R.id.fragment_container))
emojiKeyboardPopup.listener = this
text_message.listener = object : ComposerEditText.ComposerEditTextListener {
override fun onKeyboardOpened() {
if (recycler_view.isAtBottom()) {
if (adapter.itemCount > 0) {
recycler_view.scrollToPosition(0)
}
}
}
if (t.isEmpty()) {
button_send.fadeInOrOut(1F, 0F, 120)
button_show_attachment_options.fadeInOrOut(0F, 1F, 120)
playAnimation = true
override fun onKeyboardClosed() {
activity?.let {
if (!emojiKeyboardPopup.isKeyboardOpen) {
it.onBackPressed()
}
KeyboardHelper.hideSoftKeyboard(it)
emojiKeyboardPopup.dismiss()
}
})
setReactionButtonIcon(R.drawable.ic_reaction_24dp)
}
}
button_send.setOnClickListener {
var textMessage = citation ?: ""
textMessage += text_message.textContent
sendMessage(textMessage)
clearActionMessage()
}
clearMessageComposition()
}
button_show_attachment_options.setOnClickListener {
if (layout_message_attachment_options.isShown) {
......@@ -254,32 +482,101 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
}
}
view_dim.setOnClickListener { hideAttachmentOptions() }
view_dim.setOnClickListener {
hideAttachmentOptions()
}
button_files.setOnClickListener {
handler.postDelayed({
performSAF()
}, 300)
presenter.selectFile()
}, 200)
handler.postDelayed({
hideAttachmentOptions()
}, 600)
}, 400)
}
button_add_reaction.setOnClickListener { view ->
openEmojiKeyboardPopup()
}
}
}
private fun setupSuggestionsView() {
suggestions_view.anchorTo(text_message)
.setMaximumHeight(resources.getDimensionPixelSize(R.dimen.suggestions_box_max_height))
.addTokenAdapter(PeopleSuggestionsAdapter())
.addTokenAdapter(CommandSuggestionsAdapter())
.addTokenAdapter(RoomSuggestionsAdapter())
.addSuggestionProviderAction("@") { query ->
if (query.isNotEmpty()) {
presenter.spotlight(query, PEOPLE, true)
}
}
.addSuggestionProviderAction("#") { query ->
if (query.isNotEmpty()) {
presenter.loadChatRooms()
}
}
.addSuggestionProviderAction("/") { _ ->
presenter.loadCommands()
}
presenter.loadCommands()
}
private fun openEmojiKeyboardPopup() {
if (!emojiKeyboardPopup.isShowing) {
// If keyboard is visible, simply show the popup
if (emojiKeyboardPopup.isKeyboardOpen) {
emojiKeyboardPopup.showAtBottom()
} else {
// Open the text keyboard first and immediately after that show the emoji popup
text_message.isFocusableInTouchMode = true
text_message.requestFocus()
emojiKeyboardPopup.showAtBottomPending()
KeyboardHelper.showSoftKeyboard(text_message)
}
setReactionButtonIcon(R.drawable.ic_keyboard_black_24dp)
} else {
// If popup is showing, simply dismiss it to show the undelying text keyboard
emojiKeyboardPopup.dismiss()
setReactionButtonIcon(R.drawable.ic_reaction_24dp)
}
}
private fun setupActionSnackbar() {
actionSnackbar = ActionSnackbar.make(message_list_container, parser = parser)
actionSnackbar.cancelView.setOnClickListener({
clearActionMessage()
clearMessageComposition()
KeyboardHelper.showSoftKeyboard(text_message)
})
}
private fun clearActionMessage() {
citation = null
editingMessageId = null
text_message.text.clear()
actionSnackbar.dismiss()
private fun subscribeTextMessage() {
val disposable = text_message.asObservable(0)
.subscribe({ t -> setupComposeMessageButtons(t) })
compositeDisposable.add(disposable)
}
private fun unsubscribeTextMessage() {
compositeDisposable.clear()
}
private fun setupComposeMessageButtons(charSequence: CharSequence) {
if (charSequence.isNotEmpty() && playComposeMessageButtonsAnimation) {
button_show_attachment_options.fadeOut(1F, 0F, 120)
button_send.fadeIn(0F, 1F, 120)
playComposeMessageButtonsAnimation = false
}
if (charSequence.isEmpty()) {
button_send.fadeOut(1F, 0F, 120)
button_show_attachment_options.fadeIn(0F, 1F, 120)
playComposeMessageButtonsAnimation = true
}
}
private fun showAttachmentOptions() {
......@@ -298,9 +595,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
view_dim.setVisible(false)
}
private fun performSAF() {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
startActivityForResult(intent, REQUEST_CODE_FOR_PERFORM_SAF)
private fun setupToolbar(toolbarTitle: String) {
(activity as ChatRoomActivity).setupToolbarTitle(toolbarTitle)
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.ui
import android.support.v7.widget.RecyclerView
import android.text.method.LinkMovementMethod
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.AttachmentType
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.util.extensions.content
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible
import com.facebook.drawee.view.SimpleDraweeView
import com.stfalcon.frescoimageviewer.ImageViewer
import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_message.view.*
import kotlinx.android.synthetic.main.message_attachment.view.*
class PinnedMessagesAdapter : RecyclerView.Adapter<PinnedMessagesAdapter.ViewHolder>() {
init {
setHasStableIds(true)
}
val dataSet = ArrayList<MessageViewModel>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(parent.inflate(R.layout.item_message))
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(dataSet[position])
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>?) {
onBindViewHolder(holder, position)
}
override fun getItemCount(): Int = dataSet.size
override fun getItemViewType(position: Int): Int = position
fun addDataSet(dataSet: List<MessageViewModel>) {
val previousDataSetSize = this.dataSet.size
this.dataSet.addAll(previousDataSetSize, dataSet)
notifyItemRangeInserted(previousDataSetSize, dataSet.size)
}
fun addItem(message: MessageViewModel) {
dataSet.add(0, message)
notifyItemInserted(0)
}
fun updateItem(message: MessageViewModel) {
val index = dataSet.indexOfFirst { it.id == message.id }
if (index > -1) {
dataSet[index] = message
notifyItemChanged(index)
}
}
fun removeItem(messageId: String) {
val index = dataSet.indexOfFirst { it.id == messageId }
if (index > -1) {
dataSet.removeAt(index)
notifyItemRemoved(index)
}
}
override fun getItemId(position: Int): Long {
return dataSet[position].id.hashCode().toLong()
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private lateinit var messageViewModel: MessageViewModel
fun bind(message: MessageViewModel) = with(itemView) {
messageViewModel = message
image_avatar.setImageURI(message.avatarUri)
text_sender.content = message.senderName
text_message_time.content = message.time
text_content.content = message.content
text_content.movementMethod = LinkMovementMethod()
bindAttachment(message, message_attachment, image_attachment, audio_video_attachment,
file_name)
}
private fun bindAttachment(message: MessageViewModel,
attachment_container: View,
image_attachment: SimpleDraweeView,
audio_video_attachment: View,
file_name: TextView) {
with(message) {
if (attachmentUrl == null || attachmentType == null) {
attachment_container.setVisible(false)
return
}
var imageVisible = false
var videoVisible = false
attachment_container.setVisible(true)
when (message.attachmentType) {
is AttachmentType.Image -> {
imageVisible = true
image_attachment.setImageURI(message.attachmentUrl)
image_attachment.setOnClickListener { view ->
// TODO - implement a proper image viewer with a proper Transition
ImageViewer.Builder(view.context, listOf(message.attachmentUrl))
.setStartPosition(0)
.show()
}
}
is AttachmentType.Video,
is AttachmentType.Audio -> {
videoVisible = true
audio_video_attachment.setOnClickListener { view ->
message.attachmentUrl?.let { url ->
PlayerActivity.play(view.context, url)
}
}
}
}
image_attachment.setVisible(imageVisible)
audio_video_attachment.setVisible(videoVisible)
file_name.text = message.attachmentTitle
}
}
}
}
\ No newline at end of file
......@@ -9,9 +9,10 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.ChatRoomAdapter
import chat.rocket.android.chatroom.presentation.PinnedMessagesPresenter
import chat.rocket.android.chatroom.presentation.PinnedMessagesView
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast
......@@ -39,7 +40,7 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
private lateinit var chatRoomId: String
private lateinit var chatRoomName: String
private lateinit var chatRoomType: String
private lateinit var adapter: PinnedMessagesAdapter
private lateinit var adapter: ChatRoomAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
......@@ -71,10 +72,11 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showPinnedMessages(pinnedMessages: List<MessageViewModel>) {
override fun showPinnedMessages(pinnedMessages: List<BaseViewModel<*>>) {
activity?.apply {
if (recycler_view_pinned.adapter == null) {
adapter = PinnedMessagesAdapter()
// TODO - add a better constructor for this case...
adapter = ChatRoomAdapter(chatRoomType, chatRoomName, null, false)
recycler_view_pinned.adapter = adapter
val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
recycler_view_pinned.layoutManager = linearLayoutManager
......@@ -88,7 +90,7 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
}
}
adapter.addDataSet(pinnedMessages)
adapter.appendData(pinnedMessages)
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.AudioAttachment
data class AudioAttachmentViewModel(
override val message: Message,
override val rawData: AudioAttachment,
override val messageId: String,
override val attachmentUrl: String,
override val attachmentTitle: CharSequence,
override val id: Long,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null
) : BaseFileAttachmentViewModel<AudioAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.AUDIO_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.message_attachment
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
interface BaseFileAttachmentViewModel<out T> : BaseViewModel<T> {
val attachmentUrl: String
val attachmentTitle: CharSequence
val id: Long
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
interface BaseMessageViewModel<out T> : BaseViewModel<T> {
val avatar: String
val time: CharSequence
val senderName: CharSequence
val content: CharSequence
val isPinned: Boolean
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.core.model.Message
import java.security.InvalidParameterException
interface BaseViewModel<out T> {
val message: Message
val rawData: T
val messageId: String
val viewType: Int
val layoutId: Int
var reactions: List<ReactionViewModel>
var nextDownStreamMessage: BaseViewModel<*>?
enum class ViewType(val viewType: Int) {
MESSAGE(0),
SYSTEM_MESSAGE(1),
URL_PREVIEW(2),
IMAGE_ATTACHMENT(3),
VIDEO_ATTACHMENT(4),
AUDIO_ATTACHMENT(5),
MESSAGE_ATTACHMENT(6)
}
}
internal fun Int.toViewType(): BaseViewModel.ViewType {
return BaseViewModel.ViewType.values().firstOrNull { it.viewType == this }
?: throw InvalidParameterException("Invalid viewType: $this for BaseViewModel.ViewType")
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.ImageAttachment
data class ImageAttachmentViewModel(
override val message: Message,
override val rawData: ImageAttachment,
override val messageId: String,
override val attachmentUrl: String,
override val attachmentTitle: CharSequence,
override val id: Long,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null
) : BaseFileAttachmentViewModel<ImageAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.IMAGE_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.message_attachment
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
import DateTimeHelper
import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.style.AbsoluteSizeSpan
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import chat.rocket.android.R
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
import chat.rocket.common.model.Token
import chat.rocket.core.model.Message
import chat.rocket.core.model.MessageType.*
import chat.rocket.core.model.Value
import chat.rocket.core.model.attachment.*
import chat.rocket.core.model.url.Url
import okhttp3.HttpUrl
data class MessageViewModel(val context: Context,
private val token: Token?,
private val message: Message,
private val settings: Map<String, Value<Any>>,
private val parser: MessageParser,
private val messagesRepository: MessagesRepository,
private val localRepository: LocalRepository,
private val currentServerRepository: CurrentServerRepository) {
val id: String = message.id
val avatarUri: String?
val roomId: String = message.roomId
val time: CharSequence
val senderName: CharSequence
val content: CharSequence
var quote: Message? = null
var urlsWithMeta = arrayListOf<Url>()
var attachmentUrl: String? = null
var attachmentTitle: CharSequence? = null
var attachmentType: AttachmentType? = null
var attachmentMessageText: String? = null
var attachmentMessageAuthor: String? = null
var attachmentMessageIcon: String? = null
var attachmentTimestamp: Long? = null
var isSystemMessage: Boolean = false
var isPinned: Boolean = false
var currentUsername: String? = null
private val baseUrl = settings.get(SITE_URL)
init {
currentUsername = localRepository.get(LocalRepository.USERNAME_KEY)
avatarUri = getUserAvatar()
time = getTime(message.timestamp)
senderName = getSender()
isPinned = message.pinned
val baseUrl = settings.get(SITE_URL)
message.urls?.let {
if (it.isEmpty()) return@let
for (url in it) {
if (url.meta != null) {
urlsWithMeta.add(url)
}
baseUrl?.let {
val quoteUrl = HttpUrl.parse(url.url)
val serverUrl = HttpUrl.parse(baseUrl.value.toString())
if (quoteUrl != null && serverUrl != null) {
makeQuote(quoteUrl, serverUrl)
}
}
}
}
message.attachments?.let { attachments ->
val attachment = attachments.firstOrNull()
if (attachments.isEmpty() || attachment == null) return@let
when (attachment) {
is FileAttachment -> {
baseUrl?.let {
attachmentUrl = attachmentUrl("${baseUrl.value}${attachment.url}")
attachmentTitle = attachment.title
attachmentType = when (attachment) {
is ImageAttachment -> AttachmentType.Image
is VideoAttachment -> AttachmentType.Video
is AudioAttachment -> AttachmentType.Audio
else -> null
}
}
}
is MessageAttachment -> {
attachmentType = AttachmentType.Message
attachmentMessageText = attachment.text ?: ""
attachmentMessageAuthor = attachment.author ?: ""
attachmentMessageIcon = attachment.icon
attachmentTimestamp = attachment.timestamp
}
}
}
content = getContent(context)
}
private fun getUserAvatar(): String? {
val username = message.sender?.username ?: "?"
return baseUrl?.let {
UrlHelper.getAvatarUrl(baseUrl.value.toString(), username)
}
}
private fun getTime(timestamp: Long) = DateTimeHelper.getTime(DateTimeHelper.getLocalDateTime(timestamp))
private fun getSender(): CharSequence {
val useRealName = settings?.get(USE_REALNAME)?.value as Boolean
val username = message.sender?.username
val realName = message.sender?.name
val senderName = if (useRealName) realName else username
return senderName ?: context.getString(R.string.msg_unknown)
}
private fun makeQuote(quoteUrl: HttpUrl, serverUrl: HttpUrl) {
if (quoteUrl.host() == serverUrl.host()) {
val msgIdToQuote = quoteUrl.queryParameter("msg")
if (msgIdToQuote != null) {
quote = messagesRepository.getById(msgIdToQuote)
}
}
}
/**
* Get the original message as a String.
*/
fun getOriginalMessage() = message.message
private fun getContent(context: Context): CharSequence {
val contentMessage: CharSequence
when (message.type) {
//TODO: Add implementation for Welcome type.
is MessageRemoved -> contentMessage = getSystemMessage(context.getString(R.string.message_removed))
is UserJoined -> contentMessage = getSystemMessage(context.getString(R.string.message_user_joined_channel))
is UserLeft -> contentMessage = getSystemMessage(context.getString(R.string.message_user_left))
is UserAdded -> contentMessage = getSystemMessage(
context.getString(R.string.message_user_added_by, message.message, message.sender?.username))
is RoomNameChanged -> contentMessage = getSystemMessage(
context.getString(R.string.message_room_name_changed, message.message, message.sender?.username))
is UserRemoved -> contentMessage = getSystemMessage(
context.getString(R.string.message_user_removed_by, message.message, message.sender?.username))
is MessagePinned -> contentMessage = getSystemMessage(
context.getString(R.string.message_pinned))
else -> contentMessage = getNormalMessage()
}
return contentMessage
}
private fun getNormalMessage(): CharSequence {
var quoteViewModel: MessageViewModel? = null
if (quote != null) {
val quoteMessage: Message = quote!!
quoteViewModel = MessageViewModel(context, token, quoteMessage, settings, parser,
messagesRepository, localRepository, currentServerRepository)
}
return parser.renderMarkdown(message.message, quoteViewModel, currentUsername)
}
private fun getSystemMessage(content: String): CharSequence {
isSystemMessage = true
val spannableMsg = SpannableStringBuilder(content)
spannableMsg.setSpan(StyleSpan(Typeface.ITALIC), 0, spannableMsg.length,
0)
spannableMsg.setSpan(ForegroundColorSpan(Color.GRAY), 0, spannableMsg.length,
0)
if (attachmentType == null) {
val username = message.sender?.username
val message = message.message
val usernameTextStartIndex = if (username != null) content.indexOf(username) else -1
val usernameTextEndIndex = if (username != null) usernameTextStartIndex + username.length else -1
val messageTextStartIndex = if (message.isNotEmpty()) content.indexOf(message) else -1
val messageTextEndIndex = messageTextStartIndex + message.length
if (usernameTextStartIndex > -1) {
spannableMsg.setSpan(StyleSpan(Typeface.BOLD_ITALIC), usernameTextStartIndex, usernameTextEndIndex,
0)
}
if (messageTextStartIndex > -1) {
spannableMsg.setSpan(StyleSpan(Typeface.BOLD_ITALIC), messageTextStartIndex, messageTextEndIndex,
0)
}
} else if (attachmentType == AttachmentType.Message) {
spannableMsg.append(quoteMessage(attachmentMessageAuthor!!, attachmentMessageText!!, attachmentTimestamp!!))
}
return spannableMsg
}
private fun quoteMessage(author: String, text: String, timestamp: Long): CharSequence {
return SpannableStringBuilder().apply {
val header = "\n$author ${getTime(timestamp)}\n"
append(SpannableString(header).apply {
setSpan(StyleSpan(Typeface.BOLD), 1, author.length + 1, 0)
setSpan(MessageParser.QuoteMarginSpan(context.getDrawable(R.drawable.quote), 10), 1, length, 0)
setSpan(AbsoluteSizeSpan(context.resources.getDimensionPixelSize(R.dimen.message_time_text_size)),
author.length + 1, length, 0)
})
append(SpannableString(parser.renderMarkdown(text)).apply {
setSpan(MessageParser.QuoteMarginSpan(context.getDrawable(R.drawable.quote), 10), 0, length, 0)
})
}
}
private fun attachmentUrl(url: String): String {
var response = url
val httpUrl = HttpUrl.parse(url)
httpUrl?.let {
response = it.newBuilder().apply {
addQueryParameter("rc_uid", token?.userId)
addQueryParameter("rc_token", token?.authToken)
}.build().toString()
}
return response
}
}
sealed class AttachmentType {
object Image : AttachmentType()
object Video : AttachmentType()
object Audio : AttachmentType()
object Message : AttachmentType()
}
data class MessageViewModel(
override val message: Message,
override val rawData: Message,
override val messageId: String,
override val avatar: String,
override val time: CharSequence,
override val senderName: CharSequence,
override val content: CharSequence,
override val isPinned: Boolean,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
var isFirstUnread: Boolean
) : BaseMessageViewModel<Message> {
override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE.viewType
override val layoutId: Int
get() = R.layout.item_message
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
import android.content.Context
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.CurrentServerRepository
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.core.TokenRepository
import chat.rocket.core.model.Message
import chat.rocket.core.model.Value
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
import javax.inject.Inject
class MessageViewModelMapper @Inject constructor(private val context: Context,
private val tokenRepository: TokenRepository,
private val messageParser: MessageParser,
private val messagesRepository: MessagesRepository,
private val localRepository: LocalRepository,
private val currentServerRepository: CurrentServerRepository) {
suspend fun mapToViewModel(message: Message, settings: Map<String, Value<Any>>): MessageViewModel = withContext(CommonPool) {
MessageViewModel(
this@MessageViewModelMapper.context,
tokenRepository.get(),
message,
settings,
messageParser,
messagesRepository,
localRepository,
currentServerRepository
)
}
suspend fun mapToViewModelList(messageList: List<Message>, settings: Map<String, Value<Any>>): List<MessageViewModel> {
return messageList.map { MessageViewModel(context, tokenRepository.get(), it, settings,
messageParser, messagesRepository, localRepository, currentServerRepository) }
}
}
\ No newline at end of file
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
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R
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 var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null
) : BaseViewModel<Url> {
override val viewType: Int
get() = BaseViewModel.ViewType.URL_PREVIEW.viewType
override val layoutId: Int
get() = R.layout.message_url_preview
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.VideoAttachment
data class VideoAttachmentViewModel(
override val message: Message,
override val rawData: VideoAttachment,
override val messageId: String,
override val attachmentUrl: String,
override val attachmentTitle: CharSequence,
override val id: Long,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null
) : BaseFileAttachmentViewModel<VideoAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.VIDEO_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.message_attachment
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
import DateTimeHelper
import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.style.AbsoluteSizeSpan
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import chat.rocket.android.R
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.core.TokenRepository
import chat.rocket.core.model.Message
import chat.rocket.core.model.MessageType
import chat.rocket.core.model.Value
import chat.rocket.core.model.attachment.*
import chat.rocket.core.model.isSystemMessage
import chat.rocket.core.model.url.Url
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
import okhttp3.HttpUrl
import timber.log.Timber
import java.security.InvalidParameterException
import javax.inject.Inject
class ViewModelMapper @Inject constructor(private val context: Context,
private val parser: MessageParser,
private val messagesRepository: MessagesRepository,
tokenRepository: TokenRepository,
localRepository: LocalRepository,
serverInteractor: GetCurrentServerInteractor,
getSettingsInteractor: GetSettingsInteractor) {
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!!
private val baseUrl = settings.baseUrl()
private val currentUsername: String? = localRepository.get(LocalRepository.USERNAME_KEY)
private val token = tokenRepository.get()
suspend fun map(message: Message): List<BaseViewModel<*>> {
return translate(message)
}
suspend fun map(messages: List<Message>): List<BaseViewModel<*>> = withContext(CommonPool) {
val list = ArrayList<BaseViewModel<*>>(messages.size)
messages.forEach {
list.addAll(translate(it))
}
return@withContext list
}
private suspend fun translate(message: Message): List<BaseViewModel<*>> = withContext(CommonPool) {
val list = ArrayList<BaseViewModel<*>>()
message.urls?.forEach {
val url = mapUrl(message, it)
url?.let { list.add(url) }
}
message.attachments?.forEach {
val attachment = mapAttachment(message, it)
attachment?.let { list.add(attachment) }
}
mapMessage(message).let {
list.add(it)
}
return@withContext list
}
private fun mapUrl(message: Message, url: Url): BaseViewModel<*>? {
if (url.ignoreParse || url.meta == null) return null
val hostname = url.parsedUrl?.hostname ?: ""
val thumb = url.meta?.imageUrl
val title = url.meta?.title
val description = url.meta?.description
return UrlPreviewViewModel(message, url, message.id, title, hostname, description, thumb,
getReactions(message))
}
private fun mapAttachment(message: Message, attachment: Attachment): BaseViewModel<*>? {
return when (attachment) {
is FileAttachment -> mapFileAttachment(message, attachment)
else -> null
}
}
private fun mapFileAttachment(message: Message, attachment: FileAttachment): BaseViewModel<*>? {
val attachmentUrl = attachmentUrl(attachment)
val attachmentTitle = attachmentTitle(attachment)
val id = attachmentId(message, attachment)
return when (attachment) {
is ImageAttachment -> ImageAttachmentViewModel(message, attachment, message.id,
attachmentUrl, attachmentTitle, id, getReactions(message))
is VideoAttachment -> VideoAttachmentViewModel(message, attachment, message.id,
attachmentUrl, attachmentTitle, id, getReactions(message))
is AudioAttachment -> AudioAttachmentViewModel(message, attachment, message.id,
attachmentUrl, attachmentTitle, id, getReactions(message))
else -> null
}
}
private fun attachmentId(message: Message, attachment: FileAttachment): Long {
return "${message.id}_${attachment.url}".hashCode().toLong()
}
private fun attachmentTitle(attachment: FileAttachment): CharSequence {
return with(attachment) {
title?.let { return@with it }
val fileUrl = HttpUrl.parse(url)
fileUrl?.let {
return@with it.pathSegments().last()
}
return@with ""
}
}
private fun attachmentUrl(attachment: FileAttachment): String {
return with(attachment) {
if (url.startsWith("http")) return@with url
val fullUrl = "$baseUrl$url"
val httpUrl = HttpUrl.parse(fullUrl)
httpUrl?.let {
return@with it.newBuilder().apply {
addQueryParameter("rc_uid", token?.userId)
addQueryParameter("rc_token", token?.authToken)
}.build().toString()
}
// Fallback to baseUrl + url
return@with fullUrl
}
}
private suspend fun mapMessage(message: Message): MessageViewModel = withContext(CommonPool) {
val sender = getSenderName(message)
val time = getTime(message.timestamp)
val avatar = getUserAvatar(message)
val baseUrl = settings.baseUrl()
var quote: Message? = null
val urls = ArrayList<Url>()
message.urls?.let {
if (it.isEmpty()) return@let
for (url in it) {
urls.add(url)
baseUrl?.let {
val quoteUrl = HttpUrl.parse(url.url)
val serverUrl = HttpUrl.parse(baseUrl)
if (quoteUrl != null && serverUrl != null) {
quote = makeQuote(quoteUrl, serverUrl)?.let {
getMessageWithoutQuoteMarkdown(it)
}
}
}
}
}
val content = getContent(context, getMessageWithoutQuoteMarkdown(message), quote)
MessageViewModel(message = getMessageWithoutQuoteMarkdown(message), rawData = message,
messageId = message.id, avatar = avatar!!, time = time, senderName = sender,
content = content, isPinned = message.pinned, reactions = getReactions(message),
isFirstUnread = false)
}
private fun getReactions(message: Message): List<ReactionViewModel> {
val reactions = message.reactions?.let {
val list = mutableListOf<ReactionViewModel>()
it.getShortNames().forEach { shortname ->
val usernames = it.getUsernames(shortname) ?: emptyList()
val count = usernames.size
list.add(
ReactionViewModel(messageId = message.id,
shortname = shortname,
unicode = EmojiParser.parse(shortname),
count = count,
usernames = usernames)
)
}
list
}
return reactions ?: emptyList()
}
private fun getMessageWithoutQuoteMarkdown(message: Message): Message {
val baseUrl = settings.baseUrl()
return message.copy(
message = message.message.replace("\\[\\s\\]\\($baseUrl.*\\)".toRegex(), "").trim()
)
}
private fun getSenderName(message: Message): CharSequence {
if (!message.senderAlias.isNullOrEmpty()) {
return message.senderAlias!!
}
val username = message.sender?.username
val realName = message.sender?.name
val senderName = if (settings.useRealName()) realName else username
return senderName ?: username.toString()
}
private fun getUserAvatar(message: Message): String? {
message.avatar?.let {
return it // Always give preference for overridden avatar from message
}
val username = message.sender?.username ?: "?"
return baseUrl?.let {
UrlHelper.getAvatarUrl(baseUrl, username)
}
}
private fun getTime(timestamp: Long) = DateTimeHelper.getTime(DateTimeHelper.getLocalDateTime(timestamp))
private fun makeQuote(quoteUrl: HttpUrl, serverUrl: HttpUrl): Message? {
if (quoteUrl.host() == serverUrl.host()) {
val msgIdToQuote = quoteUrl.queryParameter("msg")
Timber.d("Will quote message Id: $msgIdToQuote")
return if (msgIdToQuote != null) messagesRepository.getById(msgIdToQuote) else null
}
return null
}
private suspend fun getContent(context: Context, message: Message, quote: Message?): CharSequence {
return when (message.isSystemMessage()) {
true -> getSystemMessage(message, context)
false -> getNormalMessage(message, quote)
}
}
private suspend fun getNormalMessage(message: Message, quote: Message?): CharSequence {
var quoteViewModel: MessageViewModel? = null
if (quote != null) {
val quoteMessage: Message = quote
quoteViewModel = mapMessage(quoteMessage)
}
return parser.renderMarkdown(message.message, quoteViewModel, currentUsername)
}
private fun getSystemMessage(message: Message, context: Context): CharSequence {
val content = when (message.type) {
//TODO: Add implementation for Welcome type.
is MessageType.MessageRemoved -> context.getString(R.string.message_removed)
is MessageType.UserJoined -> context.getString(R.string.message_user_joined_channel)
is MessageType.UserLeft -> context.getString(R.string.message_user_left)
is MessageType.UserAdded -> context.getString(R.string.message_user_added_by, message.message, message.sender?.username)
is MessageType.RoomNameChanged -> context.getString(R.string.message_room_name_changed, message.message, message.sender?.username)
is MessageType.UserRemoved -> context.getString(R.string.message_user_removed_by, message.message, message.sender?.username)
is MessageType.MessagePinned -> {
val attachment = message.attachments?.get(0)
val pinnedSystemMessage = context.getString(R.string.message_pinned)
if (attachment != null && attachment is MessageAttachment) {
return SpannableStringBuilder(pinnedSystemMessage)
.apply {
setSpan(StyleSpan(Typeface.ITALIC), 0, length, 0)
setSpan(ForegroundColorSpan(Color.GRAY), 0, length, 0)
}
.append(quoteMessage(attachment.author!!, attachment.text!!, attachment.timestamp!!))
}
return pinnedSystemMessage
}
else -> {
throw InvalidParameterException("Invalid message type: ${message.type}")
}
}
//isSystemMessage = true
val spannableMsg = SpannableStringBuilder(content)
spannableMsg.setSpan(StyleSpan(Typeface.ITALIC), 0, spannableMsg.length,
0)
spannableMsg.setSpan(ForegroundColorSpan(Color.GRAY), 0, spannableMsg.length,
0)
/*if (attachmentType == null) {
val username = message.sender?.username
val message = message.message
val usernameTextStartIndex = if (username != null) content.indexOf(username) else -1
val usernameTextEndIndex = if (username != null) usernameTextStartIndex + username.length else -1
val messageTextStartIndex = if (message.isNotEmpty()) content.indexOf(message) else -1
val messageTextEndIndex = messageTextStartIndex + message.length
if (usernameTextStartIndex > -1) {
spannableMsg.setSpan(StyleSpan(Typeface.BOLD_ITALIC), usernameTextStartIndex, usernameTextEndIndex,
0)
}
if (messageTextStartIndex > -1) {
spannableMsg.setSpan(StyleSpan(Typeface.BOLD_ITALIC), messageTextStartIndex, messageTextEndIndex,
0)
}
} else if (attachmentType == AttachmentType.Message) {
spannableMsg.append(quoteMessage(attachmentMessageAuthor!!, attachmentMessageText!!, attachmentTimestamp!!))
}*/
return spannableMsg
}
private fun quoteMessage(author: String, text: String, timestamp: Long): CharSequence {
return SpannableStringBuilder().apply {
val header = "\n$author ${getTime(timestamp)}\n"
append(SpannableString(header).apply {
setSpan(StyleSpan(Typeface.BOLD), 1, author.length + 1, 0)
setSpan(MessageParser.QuoteMarginSpan(context.getDrawable(R.drawable.quote), 10), 1, length, 0)
setSpan(AbsoluteSizeSpan(context.resources.getDimensionPixelSize(R.dimen.message_time_text_size)),
author.length + 1, length, 0)
})
append(SpannableString(parser.renderMarkdown(text)).apply {
setSpan(MessageParser.QuoteMarginSpan(context.getDrawable(R.drawable.quote), 10), 0, length, 0)
})
}
}
}
\ No newline at end of file
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
package chat.rocket.android.chatrooms.presentation
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetChatRoomsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.SaveChatRoomsInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.main.presentation.MainNavigator
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.ConnectionManager
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.chatRooms
import chat.rocket.android.server.infraestructure.state
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException
import chat.rocket.core.RocketChatClient
import chat.rocket.common.model.*
import chat.rocket.core.internal.model.Subscription
import chat.rocket.core.internal.realtime.*
import chat.rocket.core.internal.rest.chatRooms
import chat.rocket.core.internal.rest.logout
import chat.rocket.core.internal.rest.unregisterPushToken
import chat.rocket.core.internal.realtime.State
import chat.rocket.core.internal.realtime.StreamMessage
import chat.rocket.core.internal.realtime.Type
import chat.rocket.core.internal.rest.spotlight
import chat.rocket.core.model.ChatRoom
import chat.rocket.core.model.Room
import kotlinx.coroutines.experimental.*
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.channels.Channel
import timber.log.Timber
import javax.inject.Inject
class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
private val strategy: CancelStrategy,
private val navigator: ChatRoomsNavigator,
private val navigator: MainNavigator,
private val serverInteractor: GetCurrentServerInteractor,
private val getChatRoomsInteractor: GetChatRoomsInteractor,
private val saveChatRoomsInteractor: SaveChatRoomsInteractor,
private val localRepository: LocalRepository,
factory: RocketChatClientFactory) {
private val client: RocketChatClient = factory.create(serverInteractor.get()!!)
private val refreshSettingsInteractor: RefreshSettingsInteractor,
settingsRepository: SettingsRepository,
factory: ConnectionManagerFactory) {
private val manager: ConnectionManager = factory.create(serverInteractor.get()!!)
private val currentServer = serverInteractor.get()!!
private val client = manager.client
private var reloadJob: Deferred<List<ChatRoom>>? = null
private val settings = settingsRepository.get(currentServer)!!
private val subscriptionsChannel = Channel<StreamMessage<BaseRoom>>()
private val stateChannel = Channel<State>()
private var lastState = manager.state
fun loadChatRooms() {
refreshSettingsInteractor.refreshAsync(currentServer)
launchUI(strategy) {
view.showLoading()
subscribeStatusChange()
try {
view.updateChatRooms(loadRooms())
subscribeRoomUpdates()
} catch (e: RocketChatException) {
Timber.e(e)
view.showMessage(e.message!!)
} finally {
view.hideLoading()
}
subscribeRoomUpdates()
}
}
fun loadChatRoom(chatRoom: ChatRoom) = navigator.toChatRoom(chatRoom.id, chatRoom.name,
chatRoom.type.toString(), chatRoom.readonly ?: false)
fun loadChatRoom(chatRoom: ChatRoom) {
val roomName = if (chatRoom.type is RoomType.DirectMessage
&& chatRoom.fullName != null
&& settings.useRealName()) {
chatRoom.fullName!!
} else {
chatRoom.name
}
navigator.toChatRoom(chatRoom.id, roomName,
chatRoom.type.toString(), chatRoom.readonly ?: false,
chatRoom.lastSeen ?: -1,
chatRoom.open)
}
/**
* Gets a [ChatRoom] list from local repository.
......@@ -60,14 +82,48 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
fun chatRoomsByName(name: String) {
val currentServer = serverInteractor.get()!!
launchUI(strategy) {
val roomList = getChatRoomsInteractor.getByName(currentServer, name)
view.updateChatRooms(roomList)
try {
val roomList = getChatRoomsInteractor.getByName(currentServer, name)
if (roomList.isEmpty()) {
val (users, rooms) = client.spotlight(name)
val chatRoomsCombined = mutableListOf<ChatRoom>()
chatRoomsCombined.addAll(usersToChatRooms(users))
chatRoomsCombined.addAll(roomsToChatRooms(rooms))
view.updateChatRooms(chatRoomsCombined)
} else {
view.updateChatRooms(roomList)
}
} catch (ex: RocketChatException) {
Timber.e(ex)
}
}
}
private suspend fun usersToChatRooms(users: List<User>): List<ChatRoom> {
return users.map {
ChatRoom(it.id, RoomType.DIRECT_MESSAGE, SimpleUser(
username = it.username, name = it.name, id = null), it.name ?: "",
it.name, false, null, null, null,
null, null, false, false, false,
0L, null, 0L, null, client
)
}
}
private suspend fun roomsToChatRooms(rooms: List<Room>): List<ChatRoom> {
return rooms.map {
ChatRoom(it.id, it.type, it.user, it.name ?: "",
it.fullName, it.readonly, it.updatedAt, null, null,
it.topic, it.announcement, false, false, false,
0L, null, 0L, it.lastMessage, client
)
}
}
private suspend fun loadRooms(): List<ChatRoom> {
val chatRooms = client.chatRooms().update
val chatRooms = manager.chatRooms().update
val sortedRooms = sortRooms(chatRooms)
Timber.d("Loaded rooms: ${sortedRooms.size}")
saveChatRoomsInteractor.save(currentServer, sortedRooms)
return sortedRooms
}
......@@ -78,6 +134,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
}
private fun updateRooms() {
Timber.d("Updating Rooms")
launch {
view.updateChatRooms(getChatRoomsInteractor.get(currentServer))
}
......@@ -93,104 +150,98 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
}
}
// TODO - Temporary stuff, remove when adding DB support
private suspend fun subscribeRoomUpdates() {
client.addStateChannel(stateChannel)
private suspend fun subscribeStatusChange() {
lastState = manager.state
launch(CommonPool + strategy.jobs) {
for (status in stateChannel) {
Timber.d("Changing status to: $status")
when (status) {
State.Authenticating -> Timber.d("Authenticating")
State.Connected -> {
Timber.d("Connected")
client.subscribeSubscriptions {
Timber.d("subscriptions: $it")
}
client.subscribeRooms {
Timber.d("rooms: $it")
}
for (state in stateChannel) {
Timber.d("Got new state: $state - last: $lastState")
if (state != lastState) {
launch(UI) {
view.showConnectionState(state)
}
}
}
Timber.d("Done on statusChannel")
}
when (client.state) {
State.Connected -> {
Timber.d("Already connected")
}
else -> client.connect()
}
launch(CommonPool + strategy.jobs) {
for (message in client.roomsChannel) {
Timber.d("Got message: $message")
updateRoom(message)
if (state is State.Connected) {
reloadRooms()
updateRooms()
}
}
lastState = state
}
}
}
// TODO - Temporary stuff, remove when adding DB support
private suspend fun subscribeRoomUpdates() {
manager.addStatusChannel(stateChannel)
manager.addRoomsAndSubscriptionsChannel(subscriptionsChannel)
launch(CommonPool + strategy.jobs) {
for (message in client.subscriptionsChannel) {
for (message in subscriptionsChannel) {
Timber.d("Got message: $message")
updateSubscription(message)
when (message.data) {
is Room -> updateRoom(message as StreamMessage<Room>)
is Subscription -> updateSubscription(message as StreamMessage<Subscription>)
}
}
}
}
private fun updateRoom(message: StreamMessage<Room>) {
launchUI(strategy) {
when (message.type) {
Type.Removed -> {
removeRoom(message.data.id)
}
Type.Updated -> {
updateRoom(message.data)
}
Type.Inserted -> {
// On insertion, just get all chatrooms again, since we can't create one just
// from a Room
reloadRooms()
}
private suspend fun updateRoom(message: StreamMessage<Room>) {
Timber.d("Update Room: ${message.type} - ${message.data.id} - ${message.data.name}")
when (message.type) {
Type.Removed -> {
removeRoom(message.data.id)
}
Type.Updated -> {
updateRoom(message.data)
}
Type.Inserted -> {
// On insertion, just get all chatrooms again, since we can't create one just
// from a Room
reloadRooms()
}
updateRooms()
}
updateRooms()
}
private fun updateSubscription(message: StreamMessage<Subscription>) {
launchUI(strategy) {
when (message.type) {
Type.Removed -> {
removeRoom(message.data.roomId)
}
Type.Updated -> {
updateSubscription(message.data)
}
Type.Inserted -> {
// On insertion, just get all chatrooms again, since we can't create one just
// from a Subscription
reloadRooms()
}
private suspend fun updateSubscription(message: StreamMessage<Subscription>) {
Timber.d("Update Subscription: ${message.type} - ${message.data.id} - ${message.data.name}")
when (message.type) {
Type.Removed -> {
removeRoom(message.data.roomId)
}
Type.Updated -> {
updateSubscription(message.data)
}
Type.Inserted -> {
// On insertion, just get all chatrooms again, since we can't create one just
// from a Subscription
reloadRooms()
}
updateRooms()
}
updateRooms()
}
private suspend fun reloadRooms() {
Timber.d("realoadRooms()")
reloadJob?.cancel()
reloadJob = async(CommonPool + strategy.jobs) {
delay(1000)
Timber.d("reloading rooms after wait")
loadRooms()
try {
reloadJob = async(CommonPool + strategy.jobs) {
delay(1000)
Timber.d("reloading rooms after wait")
loadRooms()
}
reloadJob?.await()
} catch (ex: Exception) {
ex.printStackTrace()
}
reloadJob?.await()
}
// Update a ChatRoom with a Room information
private fun updateRoom(room: Room) {
Timber.d("Updating Room: ${room.id} - ${room.name}")
val chatRooms = getChatRoomsInteractor.get(currentServer).toMutableList()
val chatRoom = chatRooms.find { chatRoom -> chatRoom.id == room.id }
chatRoom?.apply {
......@@ -202,7 +253,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
room.readonly,
room.updatedAt ?: updatedAt,
timestamp,
lastModified,
lastSeen,
room.topic,
room.announcement,
default,
......@@ -221,6 +272,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
// Update a ChatRoom with a Subscription information
private fun updateSubscription(subscription: Subscription) {
Timber.d("Updating subscrition: ${subscription.id} - ${subscription.name}")
val chatRooms = getChatRoomsInteractor.get(currentServer).toMutableList()
val chatRoom = chatRooms.find { chatRoom -> chatRoom.id == subscription.roomId }
chatRoom?.apply {
......@@ -232,7 +284,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
subscription.readonly ?: readonly,
subscription.updatedAt ?: updatedAt,
subscription.timestamp ?: timestamp,
subscription.lastModified ?: lastModified,
subscription.lastSeen ?: lastSeen,
topic,
announcement,
subscription.isDefault,
......@@ -252,6 +304,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
private fun removeRoom(id: String,
chatRooms: MutableList<ChatRoom> = getChatRoomsInteractor.get(currentServer).toMutableList()) {
Timber.d("Removing ROOM: $id")
synchronized(this) {
chatRooms.removeAll { chatRoom -> chatRoom.id == id }
}
......@@ -259,35 +312,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
}
fun disconnect() {
client.removeStateChannel(stateChannel)
client.disconnect()
}
/**
* Logout from current server.
*/
fun logout() {
launchUI(strategy) {
try {
clearTokens()
client.logout()
//TODO: Add the code to unsubscribe to all subscriptions.
client.disconnect()
view.onLogout()
} catch (e: RocketChatException) {
Timber.e(e)
view.showMessage(e.message!!)
}
}
}
private suspend fun clearTokens() {
serverInteractor.clear()
val pushToken = localRepository.get(LocalRepository.KEY_PUSH_TOKEN)
if (pushToken != null) {
client.unregisterPushToken(pushToken)
localRepository.clear(LocalRepository.KEY_PUSH_TOKEN)
}
localRepository.clearAllFromServer(currentServer)
manager.removeStatusChannel(stateChannel)
manager.removeRoomsAndSubscriptionsChannel(subscriptionsChannel)
}
}
\ No newline at end of file
......@@ -2,6 +2,7 @@ package chat.rocket.android.chatrooms.presentation
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.core.internal.realtime.State
import chat.rocket.core.model.ChatRoom
interface ChatRoomsView : LoadingView, MessageView {
......@@ -9,7 +10,7 @@ interface ChatRoomsView : LoadingView, MessageView {
/**
* Shows the chat rooms.
*
* @param dataSet The data set to show.
* @param newDataSet The new data set to show.
*/
suspend fun updateChatRooms(newDataSet: List<ChatRoom>)
......@@ -18,8 +19,5 @@ interface ChatRoomsView : LoadingView, MessageView {
*/
fun showNoChatRoomsToDisplay()
/**
* User has successfully logged out from the current server.
**/
fun onLogout()
fun showConnectionState(state: State)
}
\ No newline at end of file
......@@ -3,13 +3,17 @@ package chat.rocket.android.chatrooms.ui
import DateTimeHelper
import DrawableHelper
import android.content.Context
import android.graphics.drawable.Drawable
import android.support.v4.content.ContextCompat
import android.support.v7.widget.RecyclerView
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.helper.UrlHelper
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.util.extensions.content
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.textContent
......@@ -18,8 +22,10 @@ import chat.rocket.core.model.ChatRoom
import com.facebook.drawee.view.SimpleDraweeView
import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_chat.view.*
import kotlinx.android.synthetic.main.unread_messages_badge.view.*
class ChatRoomsAdapter(private val context: Context,
private val settings: PublicSettings,
private val listener: (ChatRoom) -> Unit) : RecyclerView.Adapter<ChatRoomsAdapter.ViewHolder>() {
var dataSet: MutableList<ChatRoom> = ArrayList()
......@@ -29,8 +35,6 @@ class ChatRoomsAdapter(private val context: Context,
override fun getItemCount(): Int = dataSet.size
override fun getItemViewType(position: Int): Int = position
fun updateRooms(newRooms: List<ChatRoom>) {
dataSet.clear()
dataSet.addAll(newRooms)
......@@ -39,37 +43,71 @@ class ChatRoomsAdapter(private val context: Context,
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(chatRoom: ChatRoom) = with(itemView) {
bindAvatar(chatRoom, layout_avatar, image_avatar, image_room_avatar)
bindAvatar(chatRoom, image_avatar)
bindName(chatRoom, text_chat_name)
bindLastMessageDateTime(chatRoom, text_last_message_date_time)
bindLastMessage(chatRoom, text_last_message)
bindUnreadMessages(chatRoom, text_total_unread_messages)
if (chatRoom.alert || chatRoom.unread > 0) {
text_chat_name.setTextColor(ContextCompat.getColor(context,
R.color.colorPrimaryText))
text_last_message_date_time.setTextColor(ContextCompat.getColor(context,
R.color.colorAccent))
text_last_message.setTextColor(ContextCompat.getColor(context,
android.R.color.primary_text_light))
} else {
text_chat_name.setTextColor(ContextCompat.getColor(context,
R.color.colorSecondaryText))
text_last_message_date_time.setTextColor(ContextCompat.getColor(context,
R.color.colorSecondaryText))
text_last_message.setTextColor(ContextCompat.getColor(context,
R.color.colorSecondaryText))
}
setOnClickListener { listener(chatRoom) }
}
private fun bindAvatar(chatRoom: ChatRoom, avatarLayout: View, drawee: SimpleDraweeView, imageView: ImageView) {
val chatRoomName = chatRoom.name
if (chatRoom.type is RoomType.DirectMessage) {
drawee.setImageURI(UrlHelper.getAvatarUrl(chatRoom.client.url, chatRoomName))
imageView.setVisible(false)
avatarLayout.setVisible(true)
} else {
imageView.setImageDrawable(DrawableHelper.getTextDrawable(chatRoomName))
avatarLayout.setVisible(false)
imageView.setVisible(true)
}
private fun bindAvatar(chatRoom: ChatRoom, drawee: SimpleDraweeView) {
val avatarId = if (chatRoom.type is RoomType.DirectMessage) chatRoom.name else "@${chatRoom.name}"
drawee.setImageURI(UrlHelper.getAvatarUrl(chatRoom.client.url, avatarId))
}
private fun bindName(chatRoom: ChatRoom, textView: TextView) {
textView.textContent = chatRoom.name
var drawable = when (chatRoom.type) {
is RoomType.Channel -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_channel, context)
}
is RoomType.PrivateGroup -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_lock, context)
}
is RoomType.DirectMessage -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_dm, context)
}
else -> null
}
drawable?.let {
val wrappedDrawable = DrawableHelper.wrapDrawable(it)
val mutableDrawable = wrappedDrawable.mutate()
val color = when (chatRoom.alert || chatRoom.unread > 0) {
true -> R.color.colorPrimaryText
false -> R.color.colorSecondaryText
}
DrawableHelper.tintDrawable(mutableDrawable, context, color)
DrawableHelper.compoundDrawable(textView, mutableDrawable)
}
}
private fun bindLastMessageDateTime(chatRoom: ChatRoom, textView: TextView) {
val lastMessage = chatRoom.lastMessage
if (lastMessage != null) {
val localDateTime = DateTimeHelper.getLocalDateTime(lastMessage.timestamp)
textView.textContent = DateTimeHelper.getDate(localDateTime, context)
textView.content = DateTimeHelper.getDate(localDateTime, context)
} else {
textView.content = ""
}
}
......@@ -81,16 +119,18 @@ class ChatRoomsAdapter(private val context: Context,
val senderUsername = lastMessageSender.username
when (senderUsername) {
chatRoom.name -> {
textView.textContent = message
textView.content = message
}
// TODO Change to MySelf
// chatRoom.user?.username -> {
// holder.lastMessage.textContent = context.getString(R.string.msg_you) + ": $message"
// }
else -> {
textView.textContent = "@$senderUsername: $message"
textView.content = "@$senderUsername: $message"
}
}
} else {
textView.content = ""
}
}
......@@ -105,6 +145,7 @@ class ChatRoomsAdapter(private val context: Context,
textView.textContent = context.getString(R.string.msg_more_than_ninety_nine_unread_messages)
textView.setVisible(true)
}
else -> textView.setVisible(false)
}
}
}
......
package chat.rocket.android.chatrooms.ui
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import android.support.v7.util.DiffUtil
......@@ -10,17 +10,18 @@ import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.SearchView
import android.view.*
import chat.rocket.android.R
import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter
import chat.rocket.android.chatrooms.presentation.ChatRoomsView
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.util.extensions.*
import chat.rocket.android.widget.DividerItemDecoration
import chat.rocket.core.internal.realtime.State
import chat.rocket.core.model.ChatRoom
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_chat_rooms.*
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.launch
......@@ -28,7 +29,12 @@ import javax.inject.Inject
class ChatRoomsFragment : Fragment(), ChatRoomsView {
@Inject lateinit var presenter: ChatRoomsPresenter
@Inject lateinit var serverInteractor: GetCurrentServerInteractor
@Inject lateinit var settingsRepository: SettingsRepository
private var searchView: SearchView? = null
private val handler = Handler()
private var listJob: Job? = null
companion object {
fun newInstance() = ChatRoomsFragment()
......@@ -41,6 +47,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
}
override fun onDestroy() {
handler.removeCallbacks(dismissStatus)
presenter.disconnect()
super.onDestroy()
}
......@@ -55,12 +62,18 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
presenter.loadChatRooms()
}
override fun onDestroyView() {
listJob?.cancel()
super.onDestroyView()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.chatrooms, menu)
val searchItem = menu.findItem(R.id.action_search)
searchView = searchItem?.actionView as SearchView
searchView?.maxWidth = Integer.MAX_VALUE
searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return queryChatRoomsByName(query)
......@@ -72,23 +85,21 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
})
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.action_logout -> presenter.logout()
}
return true
}
override suspend fun updateChatRooms(newDataSet: List<ChatRoom>) {
activity.apply {
launch(UI) {
activity?.apply {
listJob?.cancel()
listJob = launch(UI) {
val adapter = recycler_view.adapter as ChatRoomsAdapter
// FIXME https://fabric.io/rocketchat3/android/apps/chat.rocket.android.dev/issues/5a90d4718cb3c2fa63b3f557?time=last-seven-days
// TODO - fix this bug to reenable DiffUtil
val diff = async(CommonPool) {
DiffUtil.calculateDiff(RoomsDiffCallback(adapter.dataSet, newDataSet))
}.await()
adapter.updateRooms(newDataSet)
diff.dispatchUpdatesTo(adapter)
if (isActive) {
adapter.updateRooms(newDataSet)
diff.dispatchUpdatesTo(adapter)
}
}
}
}
......@@ -105,12 +116,27 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun onLogout() {
override fun showConnectionState(state: State) {
activity?.apply {
finish()
val intent = Intent(this, AuthenticationActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
connection_status_text.fadeIn()
handler.removeCallbacks(dismissStatus)
when (state) {
is State.Connected -> {
connection_status_text.text = getString(R.string.status_connected)
handler.postDelayed(dismissStatus, 2000)
}
is State.Disconnected -> connection_status_text.text = getString(R.string.status_disconnected)
is State.Connecting -> connection_status_text.text = getString(R.string.status_connecting)
is State.Authenticating -> connection_status_text.text = getString(R.string.status_authenticating)
is State.Disconnecting -> connection_status_text.text = getString(R.string.status_disconnecting)
is State.Waiting -> connection_status_text.text = getString(R.string.status_waiting, state.seconds)
}
}
}
private val dismissStatus = {
if (connection_status_text != null) {
connection_status_text.fadeOut()
}
}
......@@ -121,9 +147,13 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
private fun setupRecyclerView() {
activity?.apply {
recycler_view.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
recycler_view.addItemDecoration(DividerItemDecoration(this, 144, 32))
recycler_view.addItemDecoration(DividerItemDecoration(this,
resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_start),
resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_end)))
recycler_view.itemAnimator = DefaultItemAnimator()
recycler_view.adapter = ChatRoomsAdapter(this) { chatRoom ->
// TODO - use a ViewModel Mapper instead of using settings on the adapter
recycler_view.adapter = ChatRoomsAdapter(this,
settingsRepository.get(serverInteractor.get()!!)!!) { chatRoom ->
presenter.loadChatRoom(chatRoom)
}
}
......@@ -157,4 +187,4 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
return newRooms[newItemPosition]
}
}
}
}
\ No newline at end of file
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>()*/
}
......@@ -11,10 +11,13 @@ import chat.rocket.android.chatroom.di.PinnedMessagesFragmentProvider
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.chatroom.ui.PinnedMessagesActivity
import chat.rocket.android.chatrooms.di.ChatRoomsFragmentProvider
import chat.rocket.android.chatrooms.di.ChatRoomsModule
import chat.rocket.android.dagger.scope.PerActivity
import chat.rocket.android.main.di.MainModule
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.members.di.MembersFragmentProvider
import chat.rocket.android.profile.di.ProfileFragmentProvider
import chat.rocket.android.settings.password.di.PasswordFragmentProvider
import chat.rocket.android.settings.password.ui.PasswordActivity
import dagger.Module
import dagger.android.ContributesAndroidInjector
......@@ -31,14 +34,21 @@ abstract class ActivityBuilder {
abstract fun bindAuthenticationActivity(): AuthenticationActivity
@PerActivity
@ContributesAndroidInjector(modules = [ChatRoomsModule::class, ChatRoomsFragmentProvider::class, ProfileFragmentProvider::class])
@ContributesAndroidInjector(modules = [MainModule::class,
ChatRoomsFragmentProvider::class,
ProfileFragmentProvider::class
])
abstract fun bindMainActivity(): MainActivity
@PerActivity
@ContributesAndroidInjector(modules = [ChatRoomFragmentProvider::class])
@ContributesAndroidInjector(modules = [ChatRoomFragmentProvider::class, MembersFragmentProvider::class])
abstract fun bindChatRoomActivity(): ChatRoomActivity
@PerActivity
@ContributesAndroidInjector(modules = [PinnedMessagesFragmentProvider::class])
abstract fun bindPinnedMessagesActivity(): PinnedMessagesActivity
@PerActivity
@ContributesAndroidInjector(modules = [PasswordFragmentProvider::class])
abstract fun bindPasswordActivity(): PasswordActivity
}
\ No newline at end of file
......@@ -7,7 +7,6 @@ import android.content.SharedPreferences
import chat.rocket.android.BuildConfig
import chat.rocket.android.R
import chat.rocket.android.app.RocketChatDatabase
import chat.rocket.android.app.utils.CustomImageFormatConfigurator
import chat.rocket.android.authentication.infraestructure.MemoryTokenRepository
import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository
import chat.rocket.android.dagger.qualifier.ForFresco
......@@ -39,6 +38,7 @@ import ru.noties.markwon.il.AsyncDrawableLoader
import ru.noties.markwon.spans.SpannableTheme
import timber.log.Timber
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@Module
......@@ -101,6 +101,9 @@ class AppModule {
fun provideOkHttpClient(logger: HttpLoggingInterceptor): OkHttpClient {
return OkHttpClient.Builder().apply {
addInterceptor(logger)
connectTimeout(15, TimeUnit.SECONDS)
readTimeout(20, TimeUnit.SECONDS)
writeTimeout(15, TimeUnit.SECONDS)
}.build()
}
......@@ -127,7 +130,6 @@ class AppModule {
listeners.add(RequestLoggingListener())
return OkHttpImagePipelineConfigFactory.newBuilder(context, okHttpClient)
.setImageDecoderConfig(CustomImageFormatConfigurator.createImageDecoderConfig())
.setRequestListeners(listeners)
.setDownsampleEnabled(true)
//.experiment().setBitmapPrepareToDraw(true).experiment()
......@@ -137,11 +139,7 @@ class AppModule {
@Provides
@Singleton
fun provideDraweeConfig(): DraweeConfig {
val draweeConfigBuilder = DraweeConfig.newBuilder()
CustomImageFormatConfigurator.addCustomDrawableFactories(draweeConfigBuilder)
return draweeConfigBuilder.build()
return DraweeConfig.newBuilder().build()
}
@Provides
......@@ -181,7 +179,13 @@ class AppModule {
@Provides
@Singleton
fun provideChatRoomsRepository(): ChatRoomsRepository {
fun provideRoomRepository(): RoomRepository {
return MemoryRoomRepository()
}
@Provides
@Singleton
fun provideChatRoomRepository(): ChatRoomsRepository {
return MemoryChatRoomsRepository()
}
......@@ -203,6 +207,12 @@ class AppModule {
return MemoryMessagesRepository()
}
@Provides
@Singleton
fun provideUserRepository(): UsersRepository {
return MemoryUsersRepository()
}
@Provides
@Singleton
fun provideConfiguration(context: Application, client: OkHttpClient): SpannableConfiguration {
......
package chat.rocket.android.dagger.module
import android.content.Context
import android.content.SharedPreferences
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.SharedPrefsLocalRepository
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module
class LocalModule {
@Provides
fun provideSharedPreferences(context: Context): SharedPreferences {
return context.getSharedPreferences("rocket.chat", Context.MODE_PRIVATE)
}
@Provides
@Singleton
fun provideLocalRepository(prefs: SharedPreferences): LocalRepository {
return SharedPrefsLocalRepository(prefs)
}
}
\ No newline at end of file
package chat.rocket.android.helper
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import android.view.View
object AnimationHelper {
/**
* Shakes a view.
*/
fun shakeView(viewToShake: View, x: Float = 2F, num: Int = 0) {
if (num == 6) {
viewToShake.translationX = 0.toFloat()
return
}
val animatorSet = AnimatorSet()
animatorSet.playTogether(ObjectAnimator.ofFloat(viewToShake, "translationX", dp(viewToShake.context, x)))
animatorSet.duration = 50
animatorSet.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
shakeView(viewToShake, if (num == 5) 0.toFloat() else -x, num + 1)
}
})
animatorSet.start()
}
/**
* Vibrates the smart phone.
*/
fun vibrateSmartPhone(context: Context) {
val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
if (Build.VERSION.SDK_INT >= 26) {
vibrator.vibrate(VibrationEffect.createOneShot(200, VibrationEffect.DEFAULT_AMPLITUDE))
} else {
vibrator.vibrate(200)
}
}
private fun dp(context: Context, value: Float): Float {
val density = context.resources.displayMetrics.density
val result = Math.ceil(density.times(value.toDouble()))
return result.toFloat()
}
}
\ No newline at end of file
......@@ -3,10 +3,9 @@ package chat.rocket.android.helper
import com.crashlytics.android.Crashlytics
import timber.log.Timber
class CrashlyticsTree : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String?, throwable: Throwable?) {
override fun log(priority: Int, tag: String?, message: String, throwable: Throwable?) {
Crashlytics.log(priority, tag, message)
if (throwable != null) {
......
package chat.rocket.android.helper
import android.app.Activity
import android.content.Context
import android.graphics.Rect
import android.view.View
import android.view.inputmethod.InputMethodManager
object KeyboardHelper {
......@@ -21,4 +25,29 @@ object KeyboardHelper {
val heightDiff = rootView.bottom - rect.bottom
return heightDiff > softKeyboardHeight * dm.density
}
/**
* Hide the soft keyboard.
*
* @param activity The current focused activity.
*/
fun hideSoftKeyboard(activity: Activity) {
val currentFocus = activity.currentFocus
if (currentFocus != null) {
val inputMethodManager = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(currentFocus.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
}
}
/**
* Show the soft keyboard for the given view.
*
* @param view View to receive input focus.
*/
fun showSoftKeyboard(view: View) {
if (view.requestFocus()) {
val inputMethodManager = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, InputMethodManager.SHOW_IMPLICIT)
}
}
}
\ No newline at end of file
package chat.rocket.android.helper
import android.app.Application
import android.content.ActivityNotFoundException
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Typeface
import android.content.Intent
import android.graphics.*
import android.graphics.drawable.Drawable
import android.net.Uri
import android.provider.Browser
import android.support.v4.content.ContextCompat
import android.support.v4.content.res.ResourcesCompat
import android.text.Layout
import android.text.Spannable
import android.text.Spanned
import android.text.TextPaint
import android.text.TextUtils
import android.text.style.*
import android.util.Patterns
import android.view.View
import chat.rocket.android.R
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.android.widget.emoji.EmojiRepository
import chat.rocket.android.widget.emoji.EmojiTypefaceSpan
import org.commonmark.node.AbstractVisitor
import org.commonmark.node.BlockQuote
import org.commonmark.node.Text
import ru.noties.markwon.Markwon
import ru.noties.markwon.SpannableBuilder
import ru.noties.markwon.SpannableConfiguration
import ru.noties.markwon.renderer.SpannableMarkdownVisitor
import timber.log.Timber
import java.util.regex.Pattern
import javax.inject.Inject
class MessageParser @Inject constructor(val context: Application, private val configuration: SpannableConfiguration) {
private val parser = Markwon.createParser()
private val regexUsername = Pattern.compile("([^\\S]|^)+(@[\\w.]+)",
Pattern.MULTILINE or Pattern.CASE_INSENSITIVE)
private val regexLink = Pattern.compile("(https?:\\/\\/)?(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&/=]*)",
private val regexUsername = Pattern.compile("([^\\S]|^)+(@[\\w.\\-]+)",
Pattern.MULTILINE or Pattern.CASE_INSENSITIVE)
private val selfReferList = listOf("@all", "@here")
/**
* Render a markdown text message to Spannable.
......@@ -42,47 +51,62 @@ class MessageParser @Inject constructor(val context: Application, private val co
*/
fun renderMarkdown(text: String, quote: MessageViewModel? = null, selfUsername: String? = null): CharSequence {
val builder = SpannableBuilder()
var content: String = text
// Replace all url links to markdown url syntax.
val matcher = regexLink.matcher(content)
val consumed = mutableListOf<String>()
while (matcher.find()) {
val link = matcher.group(0)
// skip usernames
if (!link.startsWith("@") && !consumed.contains(link)) {
content = content.replace(link, "[$link]($link)")
consumed.add(link)
}
}
val content = EmojiRepository.shortnameToUnicode(text, true)
val parentNode = parser.parse(toLenientMarkdown(content))
parentNode.accept(QuoteMessageBodyVisitor(context, configuration, builder))
quote?.apply {
var quoteNode = parser.parse("> $senderName $time")
parentNode.appendChild(quoteNode)
quoteNode.accept(QuoteMessageSenderVisitor(context, configuration, builder, senderName.length))
quoteNode = parser.parse("> ${toLenientMarkdown(quote.getOriginalMessage())}")
quoteNode = parser.parse("> ${toLenientMarkdown(quote.rawData.message)}")
quoteNode.accept(EmojiVisitor(builder))
quoteNode.accept(QuoteMessageBodyVisitor(context, configuration, builder))
}
parentNode.accept(LinkVisitor(builder))
parentNode.accept(EmojiVisitor(builder))
val result = builder.text()
applySpans(result, selfUsername)
return result
}
private fun applySpans(text: CharSequence, currentUser: String?) {
if (text !is Spannable) return
applyMentionSpans(text, currentUser)
}
private fun applyMentionSpans(text: CharSequence, currentUser: String?) {
val matcher = regexUsername.matcher(text)
val result = text as Spannable
while (matcher.find()) {
val user = matcher.group(2)
val start = matcher.start(2)
//TODO: should check if username actually exists prior to applying.
val linkColor = context.resources.getColor(R.color.linkTextColor)
val linkBackgroundColor = context.resources.getColor(R.color.linkBackgroundColor)
val referSelf = currentUser != null && "@$currentUser" == user
val usernameSpan = UsernameClickableSpan(linkBackgroundColor, linkColor, referSelf)
result.setSpan(usernameSpan, start, start + user.length, 0)
with(context) {
val referSelf = when (user) {
in selfReferList -> true
"@$currentUser" -> true
else -> false
}
val mentionTextColor: Int
val mentionBgColor: Int
if (referSelf) {
mentionTextColor = ResourcesCompat.getColor(resources, R.color.white, theme)
mentionBgColor = ResourcesCompat.getColor(context.resources,
R.color.colorAccent, theme)
} else {
mentionTextColor = ResourcesCompat.getColor(resources, R.color.colorAccent,
theme)
mentionBgColor = ResourcesCompat.getColor(resources,
android.R.color.transparent, theme)
}
val padding = resources.getDimensionPixelSize(R.dimen.padding_mention).toFloat()
val radius = resources.getDimensionPixelSize(R.dimen.radius_mention).toFloat()
val usernameSpan = MentionSpan(mentionBgColor, mentionTextColor, radius, padding,
referSelf)
result.setSpan(usernameSpan, start, start + user.length, 0)
}
}
}
......@@ -116,11 +140,64 @@ class MessageParser @Inject constructor(val context: Application, private val co
// set time spans
builder.setSpan(AbsoluteSizeSpan(res.getDimensionPixelSize(R.dimen.message_time_text_size)),
timeOffsetStart, builder.length())
builder.setSpan(ForegroundColorSpan(res.getColor(R.color.darkGray)),
builder.setSpan(ForegroundColorSpan(ContextCompat.getColor(context, R.color.darkGray)),
timeOffsetStart, builder.length())
}
}
class EmojiVisitor(private val builder: SpannableBuilder) : AbstractVisitor() {
override fun visit(text: Text) {
val spannable = EmojiParser.parse(text.literal)
if (spannable is Spanned) {
val spans = spannable.getSpans(0, spannable.length, EmojiTypefaceSpan::class.java)
spans.forEach {
builder.setSpan(it, spannable.getSpanStart(it), spannable.getSpanEnd(it), 0)
}
}
visitChildren(text)
}
}
class LinkVisitor(private val builder: SpannableBuilder) : AbstractVisitor() {
override fun visit(text: Text) {
// Replace all url links to markdown url syntax.
val matcher = Patterns.WEB_URL.matcher(builder.text())
val consumed = mutableListOf<String>()
while (matcher.find()) {
val link = matcher.group(0)
// skip usernames
if (!link.startsWith("@") && link !in consumed) {
builder.setSpan(object : ClickableSpan() {
override fun onClick(view: View) {
val uri = getUri(link)
val context = view.context
val intent = Intent(Intent.ACTION_VIEW, uri)
intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.packageName)
try {
context.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Timber.e("Actvity was not found for intent, $intent")
}
}
}, matcher.start(0), matcher.end(0))
consumed.add(link)
}
}
visitChildren(text)
}
private fun getUri(link: String): Uri {
val uri = Uri.parse(link)
if (uri.scheme == null) {
return Uri.parse("http://$link")
}
return uri
}
}
class QuoteMessageBodyVisitor(private val context: Context,
configuration: SpannableConfiguration,
private val builder: SpannableBuilder) : SpannableMarkdownVisitor(configuration, builder) {
......@@ -133,7 +210,9 @@ class MessageParser @Inject constructor(val context: Application, private val co
// pass to super to apply markdown
super.visit(blockQuote)
builder.setSpan(QuoteMarginSpan(context.getDrawable(R.drawable.quote), 10), length, builder.length())
val padding = context.resources.getDimensionPixelSize(R.dimen.padding_quote)
builder.setSpan(QuoteMarginSpan(context.getDrawable(R.drawable.quote), padding), length,
builder.length())
}
}
......@@ -173,24 +252,37 @@ class MessageParser @Inject constructor(val context: Application, private val co
}
}
class UsernameClickableSpan(private val linkBackgroundColor: Int,
private val linkTextColor: Int,
private val referSelf: Boolean) : ClickableSpan() {
class MentionSpan(private val backgroundColor: Int,
private val textColor: Int,
private val radius: Float,
padding: Float,
referSelf: Boolean) : ReplacementSpan() {
private val padding: Float = if (referSelf) padding else 0F
override fun onClick(widget: View) {
//TODO: Implement action when clicking on username, like showing user profile.
override fun getSize(paint: Paint,
text: CharSequence,
start: Int,
end: Int,
fm: Paint.FontMetricsInt?): Int {
return (padding + paint.measureText(text.subSequence(start, end).toString()) + padding).toInt()
}
override fun updateDrawState(ds: TextPaint) {
if (referSelf) {
ds.color = Color.WHITE
ds.typeface = Typeface.DEFAULT_BOLD
ds.bgColor = linkTextColor
} else {
ds.color = linkTextColor
ds.bgColor = linkBackgroundColor
}
ds.isUnderlineText = false
override fun draw(canvas: Canvas,
text: CharSequence,
start: Int,
end: Int,
x: Float,
top: Int,
y: Int,
bottom: Int,
paint: Paint) {
val length = paint.measureText(text.subSequence(start, end).toString())
val rect = RectF(x, top.toFloat(), x + length + padding * 2,
bottom.toFloat())
paint.setColor(backgroundColor)
canvas.drawRoundRect(rect, radius, radius, paint)
paint.setColor(textColor)
canvas.drawText(text, start, end, x + padding, y.toFloat(), paint)
}
}
......
......@@ -11,7 +11,19 @@ object UrlHelper {
* @param avatarName The avatar name.
* @return The avatar URL.
*/
fun getAvatarUrl(serverUrl: String, avatarName: String): String = removeTrailingSlash(serverUrl) + "/avatar/" + avatarName
fun getAvatarUrl(serverUrl: String, avatarName: String, format: String = "jpeg"): String =
removeTrailingSlash(serverUrl) + "/avatar/" + removeTrailingSlash(avatarName) + "?format=$format"
/**
* Returns the CAS URL.
*
* @param casLoginUrl The CAS login URL from the server settings.
* @param serverUrl The server URL.
* @param token The token to be send to the CAS server.
* @return The avatar URL.
*/
fun getCasUrl(casLoginUrl: String, serverUrl: String, token: String): String =
removeTrailingSlash(casLoginUrl) + "?service=" + removeTrailingSlash(serverUrl) + "/_cas/" + token
/**
* Returns the server's Terms of Service URL.
......
package chat.rocket.android.main.di
import android.arch.lifecycle.LifecycleOwner
import android.content.Context
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerActivity
import chat.rocket.android.main.presentation.MainNavigator
import chat.rocket.android.main.presentation.MainView
import chat.rocket.android.main.ui.MainActivity
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
class MainModule {
@Provides
@PerActivity
fun provideMainNavigator(activity: MainActivity, context: Context) = MainNavigator(activity, context)
@Provides
fun provideMainView(activity: MainActivity): MainView = activity
@Provides
fun provideLifecycleOwner(activity: MainActivity): LifecycleOwner = activity
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy = CancelStrategy(owner, jobs)
}
\ No newline at end of file
package chat.rocket.android.main.presentation
import android.content.Context
import chat.rocket.android.R
import chat.rocket.android.chatroom.ui.chatRoomIntent
import chat.rocket.android.chatrooms.ui.ChatRoomsFragment
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.profile.ui.ProfileFragment
import chat.rocket.android.settings.ui.SettingsFragment
import chat.rocket.android.util.extensions.addFragment
class MainNavigator(internal val activity: MainActivity, internal val context: Context) {
fun toChatList() {
activity.addFragment("ChatRoomsFragment", R.id.fragment_container) {
ChatRoomsFragment.newInstance()
}
}
fun toUserProfile() {
activity.addFragment("ProfileFragment", R.id.fragment_container) {
ProfileFragment.newInstance()
}
}
fun toSettings() {
activity.addFragment("SettingsFragment", R.id.fragment_container) {
SettingsFragment.newInstance()
}
}
fun toChatRoom(chatRoomId: String,
chatRoomName: String,
chatRoomType: String,
isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long,
isChatRoomSubscribed: Boolean) {
activity.startActivity(context.chatRoomIntent(chatRoomId, chatRoomName, chatRoomType,
isChatRoomReadOnly, chatRoomLastSeen, isChatRoomSubscribed))
activity.overridePendingTransition(R.anim.open_enter, R.anim.open_exit)
}
}
\ No newline at end of file
package chat.rocket.android.main.presentation
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.realtime.disconnect
import chat.rocket.core.internal.rest.logout
import chat.rocket.core.internal.rest.unregisterPushToken
import javax.inject.Inject
class MainPresenter @Inject constructor(private val view: MainView,
private val strategy: CancelStrategy,
private val navigator: MainNavigator,
private val serverInteractor: GetCurrentServerInteractor,
private val localRepository: LocalRepository,
managerFactory: ConnectionManagerFactory,
factory: RocketChatClientFactory) {
private val currentServer = serverInteractor.get()!!
private val manager = managerFactory.create(currentServer)
private val client: RocketChatClient = factory.create(currentServer)
fun toChatList() = navigator.toChatList()
fun toUserProfile() = navigator.toUserProfile()
fun toSettings() = navigator.toSettings()
/**
* Logout from current server.
*/
fun logout() {
launchUI(strategy) {
try {
clearTokens()
client.logout()
//TODO: Add the code to unsubscribe to all subscriptions.
client.disconnect()
view.onLogout()
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
}
}
}
private suspend fun clearTokens() {
serverInteractor.clear()
val pushToken = localRepository.get(LocalRepository.KEY_PUSH_TOKEN)
if (pushToken != null) {
client.unregisterPushToken(pushToken)
localRepository.clear(LocalRepository.KEY_PUSH_TOKEN)
}
localRepository.clearAllFromServer(currentServer)
}
fun connect() {
manager.connect()
}
fun disconnect() {
manager.disconnect()
}
}
\ No newline at end of file
package chat.rocket.android.main.presentation
import chat.rocket.android.core.behaviours.MessageView
interface MainView : MessageView {
/**
* User has successfully logged out from the current server.
**/
fun onLogout()
}
\ No newline at end of file
package chat.rocket.android.main.ui
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import android.view.Gravity
import android.view.MenuItem
import chat.rocket.android.R
import chat.rocket.android.chatrooms.ui.ChatRoomsFragment
import chat.rocket.android.profile.ui.ProfileFragment
import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.main.presentation.MainPresenter
import chat.rocket.android.main.presentation.MainView
import chat.rocket.android.util.extensions.showToast
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasActivityInjector
import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.app_bar.*
import javax.inject.Inject
class MainActivity : AppCompatActivity(), HasSupportFragmentInjector {
class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupportFragmentInjector {
@Inject lateinit var activityDispatchingAndroidInjector: DispatchingAndroidInjector<Activity>
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
@Inject lateinit var presenter: MainPresenter
private var isFragmentAdded: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
......@@ -26,6 +32,7 @@ class MainActivity : AppCompatActivity(), HasSupportFragmentInjector {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
presenter.connect()
setupToolbar()
setupNavigationView()
}
......@@ -33,12 +40,33 @@ class MainActivity : AppCompatActivity(), HasSupportFragmentInjector {
override fun onResume() {
super.onResume()
if (!isFragmentAdded) {
// Adding the first fragment.
addFragment("ChatRoomsFragment")
presenter.toChatList()
isFragmentAdded = true
}
}
override fun onDestroy() {
super.onDestroy()
if (isFinishing) {
presenter.disconnect()
}
}
override fun onLogout() {
finish()
val intent = Intent(this, AuthenticationActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
}
override fun showMessage(resId: Int) = showToast(resId)
override fun showMessage(message: String) = showToast(message)
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun activityInjector(): AndroidInjector<Activity> = activityDispatchingAndroidInjector
override fun supportFragmentInjector(): AndroidInjector<Fragment> = fragmentDispatchingAndroidInjector
private fun setupToolbar() {
......@@ -61,21 +89,17 @@ class MainActivity : AppCompatActivity(), HasSupportFragmentInjector {
private fun onNavDrawerItemSelected(menuItem: MenuItem) {
when (menuItem.itemId) {
R.id.action_chat_rooms -> {
addFragment("ChatRoomsFragment", R.id.fragment_container) {
ChatRoomsFragment.newInstance()
}
presenter.toChatList()
}
R.id.action_profile -> {
addFragment("ProfileFragment", R.id.fragment_container) {
ProfileFragment.newInstance()
}
presenter.toUserProfile()
}
R.id.action_settings -> {
presenter.toSettings()
}
R.id.action_logout -> {
presenter.logout()
}
}
}
private fun addFragment(tag: String) {
addFragment(tag, R.id.fragment_container) {
ChatRoomsFragment.newInstance()
}
}
}
\ No newline at end of file
package chat.rocket.android.member.ui
import android.os.Bundle
import android.support.design.widget.BottomSheetDialogFragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import chat.rocket.android.R
import chat.rocket.android.util.extensions.content
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.textContent
import kotlinx.android.synthetic.main.fragment_member_bottom_sheet.*
fun newInstance(avatarUri: String, realName: String, username: String, email: String, utcOffset: String): BottomSheetDialogFragment {
return MemberBottomSheetFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_AVATAR_URI, avatarUri)
putString(BUNDLE_REAL_NAME, realName)
putString(BUNDLE_USERNAME, username)
putString(BUNDLE_EMAIL, email)
putString(BUNDLE_UTC_OFFSET, utcOffset)
}
}
}
private const val BUNDLE_AVATAR_URI = "avatar_uri"
private const val BUNDLE_REAL_NAME = "real_name"
private const val BUNDLE_USERNAME = "username"
private const val BUNDLE_EMAIL = "email"
private const val BUNDLE_UTC_OFFSET = "utc_offset"
class MemberBottomSheetFragment: BottomSheetDialogFragment() {
private lateinit var avatarUri: String
private lateinit var realName: String
private lateinit var username: String
private lateinit var email: String
private lateinit var utcOffset: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val bundle = arguments
if (bundle != null) {
avatarUri = bundle.getString(BUNDLE_AVATAR_URI)
realName = bundle.getString(BUNDLE_REAL_NAME)
username = bundle.getString(BUNDLE_USERNAME)
email = bundle.getString(BUNDLE_EMAIL)
utcOffset = bundle.getString(BUNDLE_UTC_OFFSET)
} else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.fragment_member_bottom_sheet, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
showMemberDetails()
}
private fun showMemberDetails() {
image_bottom_sheet_avatar.setImageURI(avatarUri)
text_bottom_sheet_member_name.content = realName
text_bottom_sheet_member_username.content = username
if (email.isNotEmpty()) {
text_member_email_address.textContent = email
} else {
text_email_address.setVisible(false)
text_member_email_address.setVisible(false)
}
if (utcOffset.isNotEmpty()){
text_member_utc.content = utcOffset
} else {
text_utc.setVisible(false)
text_member_utc.setVisible(false)
}
}
}
\ No newline at end of file
package chat.rocket.android.members.adapter
import android.support.v7.widget.RecyclerView
import android.view.View
import android.view.ViewGroup
import chat.rocket.android.R
import chat.rocket.android.members.viewmodel.MemberViewModel
import chat.rocket.android.util.extensions.content
import chat.rocket.android.util.extensions.inflate
import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_member.view.*
class MembersAdapter(private val listener: (MemberViewModel) -> Unit) : RecyclerView.Adapter<MembersAdapter.ViewHolder>() {
private var dataSet: List<MemberViewModel> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MembersAdapter.ViewHolder = ViewHolder(parent.inflate(R.layout.item_member))
override fun onBindViewHolder(holder: MembersAdapter.ViewHolder, position: Int) = holder.bind(dataSet[position], listener)
override fun getItemCount(): Int = dataSet.size
fun prependData(dataSet: List<MemberViewModel>) {
this.dataSet = dataSet
notifyItemRangeInserted(0, dataSet.size)
}
fun appendData(dataSet: List<MemberViewModel>) {
val previousDataSetSize = this.dataSet.size
this.dataSet += dataSet
notifyItemRangeInserted(previousDataSetSize, dataSet.size)
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(memberViewModel: MemberViewModel, listener: (MemberViewModel) -> Unit) = with(itemView) {
image_avatar.setImageURI(memberViewModel.avatarUri)
text_member.content = memberViewModel.displayName
setOnClickListener { listener(memberViewModel) }
}
}
}
\ No newline at end of file
package chat.rocket.android.members.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.members.presentation.MembersNavigator
import chat.rocket.android.members.presentation.MembersView
import chat.rocket.android.members.ui.MembersFragment
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
@PerFragment
class MembersFragmentModule {
@Provides
fun provideChatRoomNavigator(activity: ChatRoomActivity) = MembersNavigator(activity)
@Provides
fun membersView(frag: MembersFragment): MembersView {
return frag
}
@Provides
fun provideLifecycleOwner(frag: MembersFragment): 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.members.di
import chat.rocket.android.members.ui.MembersFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class MembersFragmentProvider {
@ContributesAndroidInjector(modules = [MembersFragmentModule::class])
abstract fun provideMembersFragment(): MembersFragment
}
\ No newline at end of file
package chat.rocket.android.members.presentation
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.member.ui.newInstance
class MembersNavigator(internal val activity: ChatRoomActivity) {
fun toMemberDetails(avatarUri: String, realName: String, username: String, email: String, utcOffset: String) {
activity.apply {
newInstance(avatarUri, realName, username, email, utcOffset)
.show(supportFragmentManager, "MemberBottomSheetFragment")
}
}
}
package chat.rocket.android.members.presentation
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.members.viewmodel.MemberViewModel
import chat.rocket.android.members.viewmodel.MemberViewModelMapper
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.getMembers
import javax.inject.Inject
class MembersPresenter @Inject constructor(private val view: MembersView,
private val navigator: MembersNavigator,
private val strategy: CancelStrategy,
private val serverInteractor: GetCurrentServerInteractor,
factory: RocketChatClientFactory,
private val mapper: MemberViewModelMapper) {
private val client: RocketChatClient = factory.create(serverInteractor.get()!!)
fun loadChatRoomsMembers(chatRoomId: String, chatRoomType: String, offset: Long = 0) {
launchUI(strategy) {
try {
view.showLoading()
val members = client.getMembers(chatRoomId, roomTypeOf(chatRoomType), offset, 60)
val memberViewModels = mapper.mapToViewModelList(members.result)
view.showMembers(memberViewModels, members.total)
} catch (ex: RocketChatException) {
ex.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
}
}
}
fun toMemberDetails(memberViewModel: MemberViewModel) {
val avatarUri = memberViewModel.avatarUri.toString()
val realName = memberViewModel.realName.toString()
val username = "@${memberViewModel.username}"
val email = memberViewModel.email ?: ""
val utcOffset = memberViewModel.utcOffset.toString()
navigator.toMemberDetails(avatarUri, realName, username, email, utcOffset)
}
}
\ No newline at end of file
package chat.rocket.android.members.presentation
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.android.members.viewmodel.MemberViewModel
interface MembersView: LoadingView, MessageView {
/**
* Shows a list of members of a room.
*
* @param dataSet The data set to show.
* @param total The total number of members.
*/
fun showMembers(dataSet: List<MemberViewModel>, total: Long)
}
\ No newline at end of file
package chat.rocket.android.members.ui
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import chat.rocket.android.R
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.members.adapter.MembersAdapter
import chat.rocket.android.members.presentation.MembersPresenter
import chat.rocket.android.members.presentation.MembersView
import chat.rocket.android.members.viewmodel.MemberViewModel
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.widget.DividerItemDecoration
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_members.*
import javax.inject.Inject
fun newInstance(chatRoomId: String, chatRoomType: String): Fragment {
return MembersFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
putString(BUNDLE_CHAT_ROOM_TYPE, chatRoomType)
}
}
}
private const val BUNDLE_CHAT_ROOM_ID = "chat_room_id"
private const val BUNDLE_CHAT_ROOM_TYPE = "chat_room_type"
class MembersFragment : Fragment(), MembersView {
@Inject lateinit var presenter: MembersPresenter
private val adapter: MembersAdapter = MembersAdapter { memberViewModel -> presenter.toMemberDetails(memberViewModel) }
private val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
private lateinit var chatRoomId: String
private lateinit var chatRoomType: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
val bundle = arguments
if (bundle != null) {
chatRoomId = bundle.getString(BUNDLE_CHAT_ROOM_ID)
chatRoomType = bundle.getString(BUNDLE_CHAT_ROOM_TYPE)
} else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_members)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as AppCompatActivity).supportActionBar?.title = ""
setupRecyclerView()
presenter.loadChatRoomsMembers(chatRoomId, chatRoomType)
}
override fun showMembers(dataSet: List<MemberViewModel>, total: Long) {
activity?.apply {
setupToolbar(total)
if (adapter.itemCount == 0) {
adapter.prependData(dataSet)
if (dataSet.size >= 59) { // TODO Check why the API retorns the specified count -1
recycler_view.addOnScrollListener(object : EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore(page: Int, totalItemsCount: Int, recyclerView: RecyclerView?) {
presenter.loadChatRoomsMembers(chatRoomId, chatRoomType, page * 60L)
}
})
}
} else {
adapter.appendData(dataSet)
}
}
}
override fun showLoading() = view_loading.setVisible(true)
override fun hideLoading() = view_loading.setVisible(false)
override fun showMessage(resId: Int) = showToast(resId)
override fun showMessage(message: String) = showToast(message)
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
private fun setupRecyclerView() {
activity?.apply {
recycler_view.layoutManager = linearLayoutManager
recycler_view.addItemDecoration(DividerItemDecoration(this))
recycler_view.adapter = adapter
}
}
private fun setupToolbar(totalMembers: Long) {
(activity as ChatRoomActivity).setupToolbarTitle(getString(R.string.title_members, totalMembers))
}
}
\ No newline at end of file
package chat.rocket.android.members.viewmodel
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.server.domain.useRealName
import chat.rocket.common.model.User
import chat.rocket.core.model.Value
class MemberViewModel(private val member: User, private val settings: Map<String, Value<Any>>, private val baseUrl: String?) {
val avatarUri: String?
val displayName: String
val realName: String?
val username: String?
val email: String?
val utcOffset: Float?
init {
avatarUri = getUserAvatar()
displayName = getUserDisplayName()
realName = getUserRealName()
username = getUserUsername()
email = getUserEmail()
utcOffset = getUserUtcOffset()
}
private fun getUserAvatar(): String? {
val username = member.username ?: "?"
return baseUrl?.let {
UrlHelper.getAvatarUrl(baseUrl, username, "png")
}
}
private fun getUserDisplayName(): String {
val username = member.username
val realName = member.name
val senderName = if (settings.useRealName()) realName else username
return senderName ?: username.toString()
}
private fun getUserRealName(): String? = member.name
private fun getUserUsername(): String? = member.username
private fun getUserEmail(): String? = member.emails?.get(0)?.address
private fun getUserUtcOffset(): Float? = member.utcOffset
}
\ No newline at end of file
package chat.rocket.android.members.viewmodel
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.baseUrl
import chat.rocket.common.model.User
import chat.rocket.core.model.Value
import javax.inject.Inject
class MemberViewModelMapper @Inject constructor(serverInteractor: GetCurrentServerInteractor, getSettingsInteractor: GetSettingsInteractor) {
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)
private val baseUrl = settings.baseUrl()
fun mapToViewModelList(memberList: List<User>): List<MemberViewModel> {
return memberList.map { MemberViewModel(it, settings, baseUrl) }
}
}
\ No newline at end of file
package chat.rocket.android.profile.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.profile.presentation.ProfileView
import chat.rocket.android.profile.ui.ProfileFragment
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
@PerFragment
......@@ -22,9 +20,4 @@ class ProfileFragmentModule {
fun provideLifecycleOwner(frag: ProfileFragment): LifecycleOwner {
return frag
}
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
}
\ No newline at end of file
......@@ -9,13 +9,14 @@ import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.setAvatar
import chat.rocket.core.internal.rest.updateProfile
import javax.inject.Inject
class ProfilePresenter @Inject constructor (private val view: ProfileView,
private val strategy: CancelStrategy,
serverInteractor: GetCurrentServerInteractor,
factory: RocketChatClientFactory) {
class ProfilePresenter @Inject constructor(private val view: ProfileView,
private val strategy: CancelStrategy,
serverInteractor: GetCurrentServerInteractor,
factory: RocketChatClientFactory) {
private val serverUrl = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(serverUrl)
private lateinit var myselfId: String
......@@ -28,27 +29,30 @@ class ProfilePresenter @Inject constructor (private val view: ProfileView,
myselfId = myself.id
val avatarUrl = UrlHelper.getAvatarUrl(serverUrl, myself.username!!)
view.showProfile(
avatarUrl,
myself.name!!,
myself.username!!,
myself.emails?.get(0)?.address!!
avatarUrl,
myself.name ?: "",
myself.username ?: "",
myself.emails?.get(0)?.address!!
)
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
}
}
}
fun updateUserProfile(email: String, name: String, username: String) {
fun updateUserProfile(email: String, name: String, username: String, avatarUrl: String = "") {
launchUI(strategy) {
view.showLoading()
try {
if(avatarUrl!="") {
client.setAvatar(avatarUrl)
}
val user = client.updateProfile(myselfId, email, name, username)
view.showProfileUpdateSuccessfullyMessage()
loadUserProfile()
......@@ -56,8 +60,8 @@ class ProfilePresenter @Inject constructor (private val view: ProfileView,
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
}
......
......@@ -6,7 +6,6 @@ import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.view.ActionMode
import android.view.*
import android.widget.Toast
import chat.rocket.android.R
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.profile.presentation.ProfilePresenter
......@@ -24,6 +23,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
private lateinit var currentName: String
private lateinit var currentUsername: String
private lateinit var currentEmail: String
private lateinit var currentAvatar: String
private var actionMode: ActionMode? = null
companion object {
......@@ -54,10 +54,12 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
text_name.textContent = name
text_username.textContent = username
text_email.textContent = email
text_avatar_url.textContent = ""
currentName = name
currentUsername = username
currentName = username
currentUsername = name
currentEmail = email
currentAvatar = avatarUrl
profile_container.setVisible(true)
......@@ -93,7 +95,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
override fun onActionItemClicked(mode: ActionMode, menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_profile -> {
presenter.updateUserProfile(text_email.textContent, text_name.textContent, text_username.textContent)
presenter.updateUserProfile(text_email.textContent, text_name.textContent, text_username.textContent, text_avatar_url.textContent)
mode.finish()
true
}
......@@ -116,22 +118,31 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_person_black_24dp, this)
val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, this)
val emailDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_email_black_24dp, this)
val linkDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_link_black_24dp, this)
val drawables = arrayOf(personDrawable, atDrawable, emailDrawable)
val drawables = arrayOf(personDrawable, atDrawable, emailDrawable, linkDrawable)
DrawableHelper.wrapDrawables(drawables)
DrawableHelper.tintDrawables(drawables, this, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawables(arrayOf(text_name, text_username, text_email), drawables)
DrawableHelper.compoundDrawables(arrayOf(text_name, text_username, text_email, text_avatar_url), drawables)
}
}
private fun listenToChanges() {
Observables.combineLatest(text_name.asObservable(), text_username.asObservable(), text_email.asObservable()).subscribe({ t ->
if (t.first.toString() != currentName || t.second.toString() != currentUsername || t.third.toString() != currentEmail) {
startActionMode()
} else {
finishActionMode()
}
})
Observables.combineLatest(text_name.asObservable(),
text_username.asObservable(),
text_email.asObservable(),
text_avatar_url.asObservable()) { text_name, text_username, text_email, text_avatar_url ->
return@combineLatest (text_name.toString() != currentName ||
text_username.toString() != currentUsername ||
text_email.toString() != currentEmail ||
(text_avatar_url.toString() != "" && text_avatar_url.toString() != currentAvatar))
}.subscribe({ isValid ->
if (isValid) {
startActionMode()
} else {
finishActionMode()
}
})
}
private fun startActionMode() {
......@@ -143,8 +154,9 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
private fun finishActionMode() = actionMode?.finish()
private fun enableUserInput(value: Boolean) {
text_name.isEnabled = value
text_username.isEnabled = value
text_username.isEnabled = value
text_email.isEnabled = value
text_avatar_url.isEnabled = value
}
}
......@@ -6,6 +6,14 @@ import kotlinx.coroutines.experimental.withContext
import javax.inject.Inject
class GetChatRoomsInteractor @Inject constructor(private val repository: ChatRoomsRepository) {
/**
* Get all ChatRoom objects.
*
* @param url The server url.
*
* @return All the ChatRoom objects.
*/
fun get(url: String) = repository.get(url)
/**
......
......@@ -8,6 +8,7 @@ interface MessagesRepository {
* Get message by its message id.
*
* @param id The id of the message to get.
*
* @return The Message object given by the id or null if message wasn't found.
*/
fun getById(id: String): Message?
......@@ -20,8 +21,19 @@ interface MessagesRepository {
*/
fun getByRoomId(rid: String): List<Message>
/**
* Get most recent messages up to count different users.
*
* @param rid The id of the room the messages are.
* @param count The count last messages to get.
*
* @return List of last count messages.
*/
fun getRecentMessages(rid: String, count: Long): List<Message>
/**
* Get all messages. Use carefully!
*
* @return All messages or an empty list.
*/
fun getAll(): List<Message>
......
package chat.rocket.android.server.domain
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.core.internal.rest.settings
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.withContext
import javax.inject.Inject
class RefreshSettingsInteractor @Inject constructor(private val factory: RocketChatClientFactory,
private val repository: SettingsRepository) {
private var settingsFilter = arrayOf(
LDAP_ENABLE, CAS_ENABLE, CAS_LOGIN_URL,
ACCOUNT_REGISTRATION, ACCOUNT_LOGIN_FORM, ACCOUNT_PASSWORD_RESET, ACCOUNT_CUSTOM_FIELDS,
ACCOUNT_GOOGLE, ACCOUNT_FACEBOOK, ACCOUNT_GITHUB, ACCOUNT_LINKEDIN, ACCOUNT_METEOR,
ACCOUNT_TWITTER, ACCOUNT_WORDPRESS, ACCOUNT_GITLAB,
SITE_URL, SITE_NAME, FAVICON_512, USE_REALNAME, ALLOW_ROOM_NAME_SPECIAL_CHARS,
FAVORITE_ROOMS, UPLOAD_STORAGE_TYPE, UPLOAD_MAX_FILE_SIZE, UPLOAD_WHITELIST_MIMETYPES,
HIDE_USER_JOIN, HIDE_USER_LEAVE,
HIDE_TYPE_AU, HIDE_MUTE_UNMUTE, HIDE_TYPE_RU, ALLOW_MESSAGE_DELETING,
ALLOW_MESSAGE_EDITING, ALLOW_MESSAGE_PINNING, SHOW_DELETED_STATUS, SHOW_EDITED_STATUS)
suspend fun refresh(server: String) {
withContext(CommonPool) {
factory.create(server).let { client ->
val settings = client.settings(*settingsFilter)
repository.save(server, settings)
}
}
}
fun refreshAsync(server: String) {
async {
try {
refresh(server)
} catch (ex: Exception) {
ex.printStackTrace()
}
}
}
}
\ No newline at end of file
package chat.rocket.android.server.domain
import chat.rocket.common.model.RoomType
import chat.rocket.core.model.Room
interface RoomRepository {
/**
* Get all rooms. Use carefully!
*
* @return All rooms or an empty list.
*/
fun getAll(): List<Room>
fun get(query: Query.() -> Unit): List<Room>
/**
* Save a single room object.
*
* @param room The room object to save.
*/
fun save(room: Room)
/**
* Save a list of rooms.
*
* @param roomList The list of rooms to save.
*/
fun saveAll(roomList: List<Room>)
/**
* Removes all rooms.
*/
fun clear()
data class Query(
var id: String? = null,
var name: String? = null,
var fullName: String? = null,
var type: RoomType? = null,
var readonly: Boolean? = null
)
}
\ No newline at end of file
package chat.rocket.android.server.domain
import chat.rocket.android.util.extensions.mapToTypedArray
import chat.rocket.core.model.Value
typealias PublicSettings = Map<String, Value<Any>>
interface SettingsRepository {
fun save(url: String, settings: PublicSettings)
fun get(url: String): PublicSettings?
fun get(url: String): PublicSettings
}
// Authentication methods.
const val LDAP_ENABLE = "LDAP_Enable"
const val CAS_ENABLE = "CAS_enabled"
const val CAS_LOGIN_URL = "CAS_login_url"
const val ACCOUNT_REGISTRATION = "Accounts_RegistrationForm"
const val ACCOUNT_LOGIN_FORM = "Accounts_ShowFormLogin"
const val ACCOUNT_PASSWORD_RESET = "Accounts_PasswordReset"
const val ACCOUNT_CUSTOM_FIELDS = "Accounts_CustomFields"
const val ACCOUNT_GOOGLE = "Accounts_OAuth_Google"
const val ACCOUNT_FACEBOOK = "Accounts_OAuth_Facebook"
const val ACCOUNT_GITHUB = "Accounts_OAuth_Github"
const val ACCOUNT_GITLAB = "Accounts_OAuth_Gitlab"
const val ACCOUNT_GOOGLE = "Accounts_OAuth_Google"
const val ACCOUNT_LINKEDIN = "Accounts_OAuth_Linkedin"
const val ACCOUNT_METEOR = "Accounts_OAuth_Meteor"
const val ACCOUNT_TWITTER = "Accounts_OAuth_Twitter"
const val ACCOUNT_WORDPRESS = "Accounts_OAuth_Wordpress"
const val ACCOUNT_REGISTRATION = "Accounts_RegistrationForm"
const val ACCOUNT_LOGIN_FORM = "Accounts_ShowFormLogin"
const val ACCOUNT_CUSTOM_FIELDS = "Accounts_CustomFields"
const val ACCOUNT_GITLAB = "Accounts_OAuth_Gitlab"
const val SITE_URL = "Site_Url"
const val SITE_NAME = "Site_Name"
......@@ -27,8 +33,9 @@ const val FAVICON_512 = "Assets_favicon_512"
const val USE_REALNAME = "UI_Use_Real_Name"
const val ALLOW_ROOM_NAME_SPECIAL_CHARS = "UI_Allow_room_names_with_special_chars"
const val FAVORITE_ROOMS = "Favorite_Rooms"
const val LDAP_ENABLE = "LDAP_Enable"
const val STORAGE_TYPE = "FileUpload_Storage_Type"
const val UPLOAD_STORAGE_TYPE = "FileUpload_Storage_Type"
const val UPLOAD_MAX_FILE_SIZE = "FileUpload_MaxFileSize"
const val UPLOAD_WHITELIST_MIMETYPES = "FileUpload_MediaTypeWhiteList"
const val HIDE_USER_JOIN = "Message_HideType_uj"
const val HIDE_USER_LEAVE = "Message_HideType_ul"
const val HIDE_TYPE_AU = "Message_HideType_au"
......@@ -39,31 +46,48 @@ const val ALLOW_MESSAGE_EDITING = "Message_AllowEditing"
const val SHOW_DELETED_STATUS = "Message_ShowDeletedStatus"
const val SHOW_EDITED_STATUS = "Message_ShowEditedStatus"
const val ALLOW_MESSAGE_PINNING = "Message_AllowPinning"
/*
* Extension functions for Public Settings.
*
* If you need to access a Setting, add a const val key above, add it to the filter on
* ServerPresenter.kt and a extension function to access it
*/
fun Map<String, Value<Any>>.googleEnabled(): Boolean = this[ACCOUNT_GOOGLE]?.value == true
fun Map<String, Value<Any>>.facebookEnabled(): Boolean = this[ACCOUNT_FACEBOOK]?.value == true
fun Map<String, Value<Any>>.githubEnabled(): Boolean = this[ACCOUNT_GITHUB]?.value == true
fun Map<String, Value<Any>>.linkedinEnabled(): Boolean = this[ACCOUNT_LINKEDIN]?.value == true
fun Map<String, Value<Any>>.meteorEnabled(): Boolean = this[ACCOUNT_METEOR]?.value == true
fun Map<String, Value<Any>>.twitterEnabled(): Boolean = this[ACCOUNT_TWITTER]?.value == true
fun Map<String, Value<Any>>.gitlabEnabled(): Boolean = this[ACCOUNT_GITLAB]?.value == true
fun Map<String, Value<Any>>.wordpressEnabled(): Boolean = this[ACCOUNT_WORDPRESS]?.value == true
fun PublicSettings.isLdapAuthenticationEnabled(): Boolean = this[LDAP_ENABLE]?.value == true
fun PublicSettings.isCasAuthenticationEnabled(): Boolean = this[CAS_ENABLE]?.value == true
fun PublicSettings.casLoginUrl(): String = this[CAS_LOGIN_URL]?.value.toString()
fun PublicSettings.isRegistrationEnabledForNewUsers(): Boolean = this[ACCOUNT_REGISTRATION]?.value == "Public"
fun PublicSettings.isLoginFormEnabled(): Boolean = this[ACCOUNT_LOGIN_FORM]?.value == true
fun PublicSettings.isPasswordResetEnabled(): Boolean = this[ACCOUNT_PASSWORD_RESET]?.value == true
fun PublicSettings.isGoogleAuthenticationEnabled(): Boolean = this[ACCOUNT_GOOGLE]?.value == true
fun PublicSettings.isFacebookAuthenticationEnabled(): Boolean = this[ACCOUNT_FACEBOOK]?.value == true
fun PublicSettings.isGithubAuthenticationEnabled(): Boolean = this[ACCOUNT_GITHUB]?.value == true
fun PublicSettings.isLinkedinAuthenticationEnabled(): Boolean = this[ACCOUNT_LINKEDIN]?.value == true
fun PublicSettings.isMeteorAuthenticationEnabled(): Boolean = this[ACCOUNT_METEOR]?.value == true
fun PublicSettings.isTwitterAuthenticationEnabled(): Boolean = this[ACCOUNT_TWITTER]?.value == true
fun PublicSettings.isGitlabAuthenticationEnabled(): Boolean = this[ACCOUNT_GITLAB]?.value == true
fun PublicSettings.isWordpressAuthenticationEnabled(): Boolean = this[ACCOUNT_WORDPRESS]?.value == true
fun Map<String, Value<Any>>.useRealName(): Boolean = this[USE_REALNAME]?.value == true
fun PublicSettings.useRealName(): Boolean = this[USE_REALNAME]?.value == true
// Message settings
fun Map<String, Value<Any>>.showDeletedStatus(): Boolean = this[SHOW_DELETED_STATUS]?.value == true
fun Map<String, Value<Any>>.showEditedStatus(): Boolean = this[SHOW_EDITED_STATUS]?.value == true
fun Map<String, Value<Any>>.allowedMessagePinning(): Boolean = this[ALLOW_MESSAGE_PINNING]?.value == true
fun Map<String, Value<Any>>.allowedMessageEditing(): Boolean = this[ALLOW_MESSAGE_EDITING]?.value == true
fun Map<String, Value<Any>>.allowedMessageDeleting(): Boolean = this[ALLOW_MESSAGE_DELETING]?.value == true
fun PublicSettings.showDeletedStatus(): Boolean = this[SHOW_DELETED_STATUS]?.value == true
fun PublicSettings.showEditedStatus(): Boolean = this[SHOW_EDITED_STATUS]?.value == true
fun PublicSettings.allowedMessagePinning(): Boolean = this[ALLOW_MESSAGE_PINNING]?.value == true
fun PublicSettings.allowedMessageEditing(): Boolean = this[ALLOW_MESSAGE_EDITING]?.value == true
fun PublicSettings.allowedMessageDeleting(): Boolean = this[ALLOW_MESSAGE_DELETING]?.value == true
fun PublicSettings.uploadMimeTypeFilter(): Array<String> {
val values = this[UPLOAD_WHITELIST_MIMETYPES]?.value
values?.let { it as String }?.split(",")?.let {
return it.mapToTypedArray { it.trim() }
}
return arrayOf("*/*")
}
fun PublicSettings.uploadMaxFileSize(): Int {
return this[UPLOAD_MAX_FILE_SIZE]?.value?.let { it as Int } ?: Int.MAX_VALUE
}
fun Map<String, Value<Any>>.registrationEnabled(): Boolean {
val value = this[ACCOUNT_REGISTRATION]
return value?.value == "Public"
}
\ No newline at end of file
fun PublicSettings.baseUrl(): String? = this[SITE_URL]?.value as String
\ No newline at end of file
package chat.rocket.android.server.domain
import chat.rocket.common.model.Email
import chat.rocket.common.model.User
import chat.rocket.common.model.UserStatus
interface UsersRepository {
/**
* Get all users. Use carefully!
*
* @return All users or an empty list.
*/
fun getAll(): List<User>
fun get(query: Query.() -> Unit): List<User>
/**
* Save a single user object.
*
* @param user The user object to save.
*/
fun save(user: User)
/**
* Save a list of users.
*
* @param users The list of users to save.
*/
fun saveAll(userList: List<User>)
/**
* Removes all users.
*/
fun clear()
data class Query(
var id: String? = null,
var name: String? = null,
var username: String? = null,
var emails: List<Email>? = null,
var utfOffset: Float? = null,
var status: UserStatus? = null,
var limit: Long = 0L
)
}
\ No newline at end of file
package chat.rocket.android.server.infraestructure
import chat.rocket.common.model.BaseRoom
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.realtime.*
import chat.rocket.core.internal.rest.chatRooms
import chat.rocket.core.model.Message
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
class ConnectionManager(internal val client: RocketChatClient) {
private val statusChannelList = ArrayList<Channel<State>>()
private val statusChannel = Channel<State>()
private var connectJob: Job? = null
private val roomAndSubscriptionChannels = ArrayList<Channel<StreamMessage<BaseRoom>>>()
private val roomMessagesChannels = LinkedHashMap<String, Channel<Message>>()
private val subscriptionIdMap = HashMap<String, String>()
private var subscriptionId: String? = null
private var roomsId: String? = null
fun connect() {
if (connectJob?.isActive == true
&& (state !is State.Disconnected)) {
Timber.d("Already connected, just returning...")
return
}
// cleanup first
client.removeStateChannel(statusChannel)
client.disconnect()
connectJob?.cancel()
// Connect and setup
client.addStateChannel(statusChannel)
connectJob = launch {
for (status in statusChannel) {
Timber.d("Changing status to: $status")
when (status) {
is State.Connected -> {
client.subscribeSubscriptions { _, id ->
Timber.d("Subscribed to subscriptions: $id")
subscriptionId = id
}
client.subscribeRooms { _, id ->
Timber.d("Subscribed to rooms: $id")
roomsId = id
}
resubscribeRooms()
}
is State.Waiting -> {
Timber.d("Connection in: ${status.seconds}")
}
}
for (channel in statusChannelList) {
channel.send(status)
}
}
}
launch(parent = connectJob) {
for (room in client.roomsChannel) {
Timber.d("GOT Room streamed")
for (channel in roomAndSubscriptionChannels) {
channel.send(room)
}
}
}
launch(parent = connectJob) {
for (subscription in client.subscriptionsChannel) {
Timber.d("GOT Subscription streamed")
for (channel in roomAndSubscriptionChannels) {
channel.send(subscription)
}
}
}
launch(parent = connectJob) {
for (message in client.messagesChannel) {
Timber.d("Received new Message for room ${message.roomId}")
val channel = roomMessagesChannels[message.roomId]
channel?.send(message)
}
}
client.connect()
// Broadcast initial state...
val state = client.state
for (channel in statusChannelList) {
channel.offer(state)
}
}
private fun resubscribeRooms() {
roomMessagesChannels.toList().map { (roomId, channel) ->
client.subscribeRoomMessages(roomId) { _, id ->
Timber.d("Subscribed to $roomId: $id")
subscriptionIdMap[roomId] = id
}
}
}
fun disconnect() {
Timber.d("ConnectionManager DISCONNECT")
client.removeStateChannel(statusChannel)
client.disconnect()
connectJob?.cancel()
}
fun addStatusChannel(channel: Channel<State>) = statusChannelList.add(channel)
fun removeStatusChannel(channel: Channel<State>) = statusChannelList.remove(channel)
fun addRoomsAndSubscriptionsChannel(channel: Channel<StreamMessage<BaseRoom>>) = roomAndSubscriptionChannels.add(channel)
fun removeRoomsAndSubscriptionsChannel(channel: Channel<StreamMessage<BaseRoom>>) = roomAndSubscriptionChannels.remove(channel)
fun subscribeRoomMessages(roomId: String, channel: Channel<Message>) {
val oldSub = roomMessagesChannels.put(roomId, channel)
if (oldSub != null) {
Timber.d("Room $roomId already subscribed...")
return
}
if (client.state is State.Connected) {
client.subscribeRoomMessages(roomId) { _, id ->
Timber.d("Subscribed to $roomId: $id")
subscriptionIdMap[roomId] = id
}
}
}
fun unsubscribeRoomMessages(roomId: String) {
val sub = roomMessagesChannels.remove(roomId)
if (sub != null) {
val id = subscriptionIdMap.remove(roomId)
id?.let { client.unsubscribe(it) }
}
}
}
suspend fun ConnectionManager.chatRooms(timestamp: Long = 0, filterCustom: Boolean = true)
= client.chatRooms(timestamp, filterCustom)
val ConnectionManager.state: State
get() = client.state
\ No newline at end of file
package chat.rocket.android.server.infraestructure
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ConnectionManagerFactory @Inject constructor(private val factory: RocketChatClientFactory) {
private val cache = HashMap<String, ConnectionManager>()
fun create(url: String): ConnectionManager {
cache[url]?.let {
Timber.d("Returning CACHED Manager for: $url")
return it
}
Timber.d("Returning FRESH Manager for: $url")
val manager = ConnectionManager(factory.create(url))
cache[url] = manager
return manager
}
}
\ No newline at end of file
......@@ -15,6 +15,11 @@ class MemoryMessagesRepository : MessagesRepository {
return messages.filter { it.value.roomId == rid }.values.toList()
}
override fun getRecentMessages(rid: String, count: Long): List<Message> {
return getByRoomId(rid).sortedByDescending { it.timestamp }
.distinctBy { it.sender }.take(count.toInt())
}
override fun getAll(): List<Message> = messages.values.toList()
override fun save(message: Message) {
......
package chat.rocket.android.server.infraestructure
import chat.rocket.android.server.domain.RoomRepository
import chat.rocket.android.server.domain.RoomRepository.Query
import chat.rocket.core.model.Room
import java.util.concurrent.CopyOnWriteArrayList
class MemoryRoomRepository : RoomRepository {
private val rooms = CopyOnWriteArrayList<Room>()
override fun getAll() = rooms.toList()
override fun get(query: Query.() -> Unit): List<Room> {
val q = Query().apply(query)
return rooms.filter {
with(q) {
if (name != null && it.name?.contains(name!!.toRegex()) == true) return@filter false
if (fullName != null && it.fullName?.contains(fullName!!.toRegex()) == true) return@filter false
if (id != null && id == it.id) return@filter false
if (readonly != null && readonly == it.readonly) return@filter false
if (type != null && type == it.type) return@filter false
return@filter true
}
}
}
override fun save(room: Room) {
rooms.addIfAbsent(room)
}
override fun saveAll(roomList: List<Room>) {
rooms.addAllAbsent(roomList)
}
override fun clear() {
rooms.clear()
}
}
\ No newline at end of file
package chat.rocket.android.server.infraestructure
import chat.rocket.android.server.domain.UsersRepository
import chat.rocket.android.server.domain.UsersRepository.Query
import chat.rocket.common.model.User
import java.util.concurrent.CopyOnWriteArrayList
class MemoryUsersRepository : UsersRepository {
private val users = CopyOnWriteArrayList<User>()
override fun getAll(): List<User> {
return users.toList()
}
override fun get(query: Query.() -> Unit): List<User> {
val q = Query().apply(query)
return users.filter {
with(q) {
if (name != null && it.name?.contains(name!!.toRegex()) == true) return@filter false
if (username != null && it.username?.contains(username!!.toRegex()) == true) return@filter false
if (id != null && id == it.id) return@filter false
if (status != null && status == it.status) return@filter false
return@filter true
}
}
}
override fun save(user: User) {
users.addIfAbsent(user)
}
override fun saveAll(userList: List<User>) {
users.addAllAbsent(userList)
}
override fun clear() {
this.users.clear()
}
}
\ No newline at end of file
......@@ -28,7 +28,7 @@ class RocketChatClientFactory @Inject constructor(val okHttpClient: OkHttpClient
}
Timber.d("Returning NEW client for: $url")
cache.put(url, client)
cache[url] = client
return client
}
}
\ No newline at end of file
......@@ -2,24 +2,22 @@ package chat.rocket.android.server.infraestructure
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.LocalRepository.Companion.SETTINGS_KEY
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.core.internal.SettingsAdapter
import chat.rocket.core.model.Value
class SharedPreferencesSettingsRepository(private val localRespository: LocalRepository) : SettingsRepository {
class SharedPreferencesSettingsRepository(private val localRepository: LocalRepository) : SettingsRepository {
private val adapter = SettingsAdapter()
private val adapter = SettingsAdapter().lenient()
override fun save(url: String, settings: Map<String, Value<Any>>) {
localRespository.save("$SETTINGS_KEY$url", adapter.toJson(settings))
override fun save(url: String, settings: PublicSettings) {
localRepository.save("$SETTINGS_KEY$url", adapter.toJson(settings))
}
override fun get(url: String): Map<String, Value<Any>>? {
val settings = localRespository.get("$SETTINGS_KEY$url")
settings?.let {
return adapter.fromJson(it)
override fun get(url: String): PublicSettings {
val settings = localRepository.get("$SETTINGS_KEY$url")!!
settings.let {
return adapter.fromJson(it)!!
}
return null
}
}
\ No newline at end of file
package chat.rocket.android.settings.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.settings.presentation.SettingsView
import chat.rocket.android.settings.ui.SettingsFragment
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
@PerFragment
class SettingsFragmentModule {
@Provides
fun settingsView(frag: SettingsFragment): SettingsView {
return frag
}
@Provides
fun settingsLifecycleOwner(frag: SettingsFragment): LifecycleOwner {
return frag
}
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
}
\ No newline at end of file
package chat.rocket.android.settings.di
import chat.rocket.android.settings.ui.SettingsFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class SettingsFragmentProvider {
@ContributesAndroidInjector(modules = [SettingsFragmentModule::class])
abstract fun provideSettingsFragment(): SettingsFragment
}
\ No newline at end of file
package chat.rocket.android.settings.password.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.settings.password.presentation.PasswordView
import chat.rocket.android.settings.password.ui.PasswordFragment
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
@PerFragment
class PasswordFragmentModule {
@Provides
fun passwordView(frag: PasswordFragment): PasswordView {
return frag
}
@Provides
fun settingsLifecycleOwner(frag: PasswordFragment): LifecycleOwner {
return frag
}
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
}
package chat.rocket.android.settings.password.di
import chat.rocket.android.settings.password.ui.PasswordFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class PasswordFragmentProvider {
@ContributesAndroidInjector(modules = [PasswordFragmentModule::class])
abstract fun providePasswordFragment(): PasswordFragment
}
\ No newline at end of file
package chat.rocket.android.settings.password.presentation
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.updateProfile
import javax.inject.Inject
class PasswordPresenter @Inject constructor (private val view: PasswordView,
private val strategy: CancelStrategy,
serverInteractor: GetCurrentServerInteractor,
factory: RocketChatClientFactory){
private val serverUrl = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(serverUrl)
fun updatePassword(password: String) {
launchUI(strategy) {
try {
view.showLoading()
client.updateProfile(client.me().id, null, null, password, null)
view.showPasswordSuccessfullyUpdatedMessage()
view.hideLoading()
} catch (exception: RocketChatException) {
view.showPasswordFailsUpdateMessage(exception.message)
view.hideLoading()
}
}
}
}
\ No newline at end of file
package chat.rocket.android.settings.password.presentation
import chat.rocket.android.core.behaviours.LoadingView
interface PasswordView: LoadingView {
/**
* Shows a message when a user's password is successfully updated
*/
fun showPasswordSuccessfullyUpdatedMessage()
/**
* Shows a message when the user's password fails to update
* @param error is a String containing the failure message
*/
fun showPasswordFailsUpdateMessage(error : String?)
}
package chat.rocket.android.settings.password.ui
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import chat.rocket.android.R
import chat.rocket.android.chatrooms.ui.ChatRoomsFragment
import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.util.extensions.textContent
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.app_bar_password.*
import javax.inject.Inject
class PasswordActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_password)
setupToolbar()
addFragment("PasswordFragment")
}
override fun onBackPressed() {
super.onBackPressed()
finish()
overridePendingTransition(R.anim.close_enter, R.anim.close_exit)
}
override fun onSupportNavigateUp(): Boolean {
onBackPressed()
return super.onNavigateUp()
}
override fun supportFragmentInjector(): AndroidInjector<Fragment> = fragmentDispatchingAndroidInjector
private fun addFragment(tag: String) {
addFragment(tag, R.id.fragment_container) {
PasswordFragment.newInstance()
}
}
private fun setupToolbar() {
setSupportActionBar(toolbar)
text_change_password.textContent = resources.getString(R.string.title_password)
}
}
package chat.rocket.android.settings.password.ui
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.*
import android.widget.Toast
import chat.rocket.android.R
import chat.rocket.android.settings.password.presentation.PasswordPresenter
import chat.rocket.android.settings.password.presentation.PasswordView
import chat.rocket.android.util.extensions.asObservable
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.textContent
import android.support.v7.view.ActionMode
import dagger.android.support.AndroidSupportInjection
import io.reactivex.rxkotlin.Observables
import kotlinx.android.synthetic.main.fragment_password.*
import javax.inject.Inject
class PasswordFragment: Fragment(), PasswordView, android.support.v7.view.ActionMode.Callback {
@Inject lateinit var presenter: PasswordPresenter
private var actionMode: ActionMode? = null
companion object {
fun newInstance() = PasswordFragment()
}
override fun onCreate(savedInstanceState: Bundle?) {
AndroidSupportInjection.inject(this)
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_password)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
listenToChanges()
}
override fun hideLoading() {
layout_new_password.visibility = View.VISIBLE
layout_confirm_password.visibility = View.VISIBLE
view_loading.visibility = View.GONE
}
override fun onActionItemClicked(mode: ActionMode, menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_password -> {
presenter.updatePassword(text_new_password.textContent)
mode.finish()
return true
}
else -> {
false
}
}
}
override fun onCreateActionMode(mode: ActionMode, menu: Menu?): Boolean {
mode.menuInflater.inflate(R.menu.password, menu)
mode.title = resources.getString(R.string.action_confirm_password)
return true
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean = false
override fun onDestroyActionMode(mode: ActionMode?) {
actionMode = null
}
override fun showLoading() {
layout_new_password.visibility = View.GONE
layout_confirm_password.visibility = View.GONE
view_loading.visibility = View.VISIBLE
}
override fun showPasswordFailsUpdateMessage(error: String?) {
showToast("Password fails to update: " + error)
}
override fun showPasswordSuccessfullyUpdatedMessage() {
showToast("Password was successfully updated!")
}
private fun finishActionMode() = actionMode?.finish()
private fun listenToChanges() {
Observables.combineLatest(text_new_password.asObservable(), text_confirm_password.asObservable()).subscribe {
val textPassword = text_new_password.textContent
val textConfirmPassword = text_confirm_password.textContent
if (textPassword.length > 5 && textConfirmPassword.length > 5 && textPassword.equals(textConfirmPassword))
startActionMode()
else
finishActionMode()
}
}
private fun showToast(msg: String?) {
Toast.makeText(context, msg, Toast.LENGTH_LONG).show()
}
private fun startActionMode() {
if (actionMode == null) {
actionMode = (activity as PasswordActivity).startSupportActionMode(this)
}
}
}
\ No newline at end of file
package chat.rocket.android.settings.presentation
interface SettingsView
package chat.rocket.android.settings.ui
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import chat.rocket.android.R
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.settings.password.ui.PasswordActivity
import chat.rocket.android.settings.presentation.SettingsView
import chat.rocket.android.util.extensions.inflate
import kotlinx.android.synthetic.main.app_bar.*
import kotlinx.android.synthetic.main.fragment_settings.*
import kotlin.reflect.KClass
class SettingsFragment: Fragment(), SettingsView, AdapterView.OnItemClickListener {
companion object {
fun newInstance() = SettingsFragment()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_settings)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar()
setupListView()
}
override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
when (parent?.getItemAtPosition(position).toString()) {
"Change Password" -> {
startNewActivity(PasswordActivity::class)
}
}
}
private fun setupListView() {
settings_list.onItemClickListener = this
}
private fun setupToolbar() {
(activity as MainActivity).toolbar.title = getString(R.string.title_settings)
}
private fun startNewActivity(classType: KClass<out AppCompatActivity>) {
startActivity(Intent(activity, classType.java))
activity?.overridePendingTransition(R.anim.open_enter, R.anim.open_exit)
}
}
package chat.rocket.android.util.extensions
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import android.support.v4.app.Fragment
import android.view.View
import android.view.ViewAnimationUtils
import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator
fun View.rotateBy(value: Float, duration: Long = 200) {
fun View.rotateBy(value: Float, duration: Long = 100) {
animate()
.rotationBy(value)
.setDuration(duration)
.start()
}
fun View.fadeInOrOut(startValue: Float, finishValue: Float, duration: Long = 200) {
fun View.fadeIn(startValue: Float = 0f, finishValue: Float = 1f, duration: Long = 200) {
if (alpha == finishValue) {
setVisible(true)
return
}
animate()
.alpha(startValue)
.setDuration(duration)
......@@ -24,14 +38,30 @@ fun View.fadeInOrOut(startValue: Float, finishValue: Float, duration: Long = 200
.setInterpolator(AccelerateInterpolator()).start()
}).start()
if (startValue > finishValue) {
setVisible(true)
}
fun View.fadeOut(startValue: Float = 1f, finishValue: Float = 0f, duration: Long = 200) {
if (alpha == finishValue) {
setVisible(false)
} else {
setVisible(true)
return
}
animate()
.alpha(startValue)
.setDuration(duration)
.setInterpolator(DecelerateInterpolator())
.withEndAction({
animate()
.alpha(finishValue)
.setDuration(duration)
.setInterpolator(AccelerateInterpolator()).start()
}).start()
setVisible(false)
}
fun View.circularRevealOrUnreveal(centerX: Int, centerY: Int, startRadius: Float, endRadius: Float, duration: Long = 600) {
fun View.circularRevealOrUnreveal(centerX: Int, centerY: Int, startRadius: Float, endRadius: Float, duration: Long = 200) {
val anim = ViewAnimationUtils.createCircularReveal(this, centerX, centerY, startRadius, endRadius)
anim.duration = duration
......@@ -42,4 +72,37 @@ fun View.circularRevealOrUnreveal(centerX: Int, centerY: Int, startRadius: Float
}
anim.start()
}
fun View.shake(x: Float = 2F, num: Int = 0){
if (num == 6) {
this.translationX = 0.toFloat()
return
}
val animatorSet = AnimatorSet()
animatorSet.playTogether(ObjectAnimator.ofFloat(this, "translationX", this.context.dp(x)))
animatorSet.duration = 50
animatorSet.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
shake(if (num == 5) 0.toFloat() else -x, num + 1)
}
})
animatorSet.start()
}
fun Context.dp(value: Float): Float {
val density = this.resources.displayMetrics.density
val result = Math.ceil(density.times(value.toDouble()))
return result.toFloat()
}
fun Fragment.vibrateSmartPhone() {
val vibrator = context?.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
if (Build.VERSION.SDK_INT >= 26) {
vibrator.vibrate(VibrationEffect.createOneShot(200, VibrationEffect.DEFAULT_AMPLITUDE))
} else {
vibrator.vibrate(200)
}
}
\ No newline at end of file
package chat.rocket.android.util.extensions
inline fun <T, reified R> List<T>.mapToTypedArray(transform: (T) -> R): Array<R> {
return when (this) {
is RandomAccess -> Array(size) { index -> transform(this[index]) }
else -> with(iterator()) { Array(size) { transform(next()) } }
}
}
\ No newline at end of file
package chat.rocket.android.util.extensions
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import android.text.Spannable
import android.text.Spanned
import android.text.TextUtils
import android.util.Patterns
import android.widget.EditText
import android.widget.TextView
import android.provider.OpenableColumns
import android.webkit.MimeTypeMap
import android.provider.MediaStore
import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.android.widget.emoji.EmojiTypefaceSpan
import ru.noties.markwon.Markwon
import java.security.SecureRandom
fun String.ifEmpty(value: String): String {
if (isEmpty()) {
......@@ -23,6 +25,27 @@ fun CharSequence.ifEmpty(value: String): CharSequence {
return this
}
fun EditText.erase() {
this.text.clear()
val spans = this.text.getSpans(0, text.length, EmojiTypefaceSpan::class.java)
spans.forEach {
text.removeSpan(it)
}
}
fun String.isEmailValid(): Boolean = Patterns.EMAIL_ADDRESS.matcher(this).matches()
fun generateRandomString(stringLength: Int): String {
val base = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
val secureRandom = SecureRandom()
val stringBuilder = StringBuilder(stringLength)
for (i in 0 until stringLength) {
stringBuilder.append(base[secureRandom.nextInt(base.length)])
}
return stringBuilder.toString()
}
var TextView.textContent: String
get() = text.toString()
set(value) {
......@@ -35,58 +58,20 @@ var TextView.hintContent: String
hint = value
}
var TextView.content: CharSequence
var TextView.content: CharSequence?
get() = text
set(value) {
Markwon.unscheduleDrawables(this)
Markwon.unscheduleTableRows(this)
text = value
if (value is Spanned) {
val result = EmojiParser.parse(value.toString()) as Spannable
val end = if (value.length > result.length) result.length else value.length
TextUtils.copySpansFrom(value, 0, end, Any::class.java, result, 0)
text = result
} else {
val result = EmojiParser.parse(value.toString()) as Spannable
text = result
}
Markwon.scheduleDrawables(this)
Markwon.scheduleTableRows(this)
}
fun Uri.getFileName(context: Context): String? {
val cursor = context.contentResolver.query(this, null, null, null, null, null)
var fileName: String? = null
cursor.use { cursor ->
if (cursor != null && cursor.moveToFirst()) {
fileName = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
}
}
return fileName
}
fun Uri.getFileSize(context: Context): String? {
val cursor = context.contentResolver.query(this, null, null, null, null, null)
var fileSize: String? = null
cursor.use { cursor ->
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
if (cursor != null && cursor.moveToFirst()) {
if (!cursor.isNull(sizeIndex)) {
fileSize = cursor.getString(sizeIndex)
}
}
}
return fileSize
}
fun Uri.getMimeType(context: Context): String {
return if (scheme == ContentResolver.SCHEME_CONTENT) {
context.contentResolver.getType(this)
} else {
val fileExtension = MimeTypeMap.getFileExtensionFromUrl(toString())
MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.toLowerCase())
}
}
fun Uri.getRealPathFromURI(context: Context): String? {
val cursor = context.contentResolver.query(this, arrayOf(MediaStore.Images.Media.DATA), null, null, null)
cursor.use { cursor ->
val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
cursor.moveToFirst()
return cursor.getString(columnIndex)
}
}
\ No newline at end of file
}
\ No newline at end of file
......@@ -6,6 +6,8 @@ import android.support.annotation.LayoutRes
import android.support.annotation.StringRes
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
......@@ -21,6 +23,10 @@ fun View.setVisible(visible: Boolean) {
}
}
fun View.isVisible(): Boolean {
return visibility == View.VISIBLE
}
fun ViewGroup.inflate(@LayoutRes resource: Int): View = LayoutInflater.from(context).inflate(resource, this, false)
fun AppCompatActivity.addFragment(tag: String, layoutId: Int, newInstance: () -> Fragment) {
......@@ -40,8 +46,10 @@ fun AppCompatActivity.addFragmentBackStack(tag: String, layoutId: Int, newInstan
}
fun Activity.hideKeyboard() {
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(currentFocus.windowToken, InputMethodManager.RESULT_UNCHANGED_SHOWN)
if (currentFocus != null) {
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(currentFocus.windowToken, InputMethodManager.RESULT_UNCHANGED_SHOWN)
}
}
fun Activity.showToast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT) = showToast(getString(resource), duration)
......@@ -50,4 +58,13 @@ fun Activity.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) = To
fun Fragment.showToast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT) = showToast(getString(resource), duration)
fun Fragment.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) = activity!!.showToast(message, duration)
\ No newline at end of file
fun Fragment.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) = activity!!.showToast(message, duration)
fun RecyclerView.isAtBottom(): Boolean {
val manager: RecyclerView.LayoutManager? = layoutManager
if (manager is LinearLayoutManager) {
return manager.findFirstVisibleItemPosition() == 0
}
return false // or true??? we can't determine the first visible item.
}
\ No newline at end of file
package chat.rocket.android.util.extensions
import android.annotation.TargetApi
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import android.os.Build
import android.provider.DocumentsContract
import android.provider.MediaStore
import android.provider.OpenableColumns
import android.webkit.MimeTypeMap
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
fun Uri.getFileName(context: Context): String? {
val cursor = context.contentResolver.query(this, null, null, null, null, null)
var fileName: String? = null
cursor.use { cursor ->
if (cursor != null && cursor.moveToFirst()) {
fileName = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
}
}
return fileName
}
fun Uri.getFileSize(context: Context): Int {
val cursor = context.contentResolver.query(this, null, null, null, null, null)
var fileSize: String? = null
cursor.use { cursor ->
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
if (cursor != null && cursor.moveToFirst()) {
if (!cursor.isNull(sizeIndex)) {
fileSize = cursor.getString(sizeIndex)
}
}
}
return fileSize?.toIntOrNull() ?: -1
}
fun Uri.getMimeType(context: Context): String {
return if (scheme == ContentResolver.SCHEME_CONTENT) {
context.contentResolver.getType(this)
} else {
val fileExtension = MimeTypeMap.getFileExtensionFromUrl(toString())
MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.toLowerCase())
}
}
fun Uri.getRealPathFromURI(context: Context): String? {
val cursor = context.contentResolver.query(this, arrayOf(MediaStore.Images.Media.DATA), null, null, null)
cursor.use { cursor ->
val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
cursor.moveToFirst()
return cursor.getString(columnIndex)
}
}
@TargetApi(Build.VERSION_CODES.N)
fun Uri.isVirtualFile(context: Context): Boolean {
if (!DocumentsContract.isDocumentUri(context, this)) {
return false
}
val cursor = context.contentResolver.query(this,
arrayOf(DocumentsContract.Document.COLUMN_FLAGS),
null, null, null)
var flags = 0
if (cursor.moveToFirst()) {
flags = cursor.getInt(0)
}
cursor.close()
return flags and DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT != 0
}
@Throws(IOException::class)
fun Uri.getInputStreamForVirtualFile(context: Context, mimeTypeFilter: String): FileInputStream? {
val resolver = context.contentResolver
val openableMimeTypes = resolver.getStreamTypes(this, mimeTypeFilter)
if (openableMimeTypes == null || openableMimeTypes.isEmpty()) {
throw FileNotFoundException()
}
return resolver.openTypedAssetFileDescriptor(this, openableMimeTypes[0],
null)?.createInputStream()
}
fun Uri.getInputStream(context: Context): InputStream? {
if (isVirtualFile(context)) {
return getInputStreamForVirtualFile(context, "*/*")
}
return context.contentResolver.openInputStream(this)
}
\ No newline at end of file
package chat.rocket.android.webview.cas.ui
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.webkit.WebView
import android.webkit.WebViewClient
import chat.rocket.android.R
import kotlinx.android.synthetic.main.activity_web_view.*
import kotlinx.android.synthetic.main.app_bar.*
fun Context.webViewIntent(webPageUrl: String, casToken: String): Intent {
return Intent(this, CasWebViewActivity::class.java).apply {
putExtra(INTENT_WEB_PAGE_URL, webPageUrl)
putExtra(INTENT_CAS_TOKEN, casToken)
}
}
private const val INTENT_WEB_PAGE_URL = "web_page_url"
private const val INTENT_CAS_TOKEN = "cas_token"
class CasWebViewActivity : AppCompatActivity() {
private lateinit var webPageUrl: String
private lateinit var casToken: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_web_view)
webPageUrl = intent.getStringExtra(INTENT_WEB_PAGE_URL)
requireNotNull(webPageUrl) { "no web_page_url provided in Intent extras" }
casToken = intent.getStringExtra(INTENT_CAS_TOKEN)
requireNotNull(casToken) { "no cas_token provided in Intent extras" }
setupToolbar()
}
override fun onResume() {
super.onResume()
setupWebView()
}
override fun onBackPressed() {
if (web_view.canGoBack()) {
web_view.goBack()
} else {
finishActivity(false)
}
}
private fun setupToolbar() {
toolbar.title = getString(R.string.title_authentication)
toolbar.setNavigationIcon(R.drawable.ic_close_white_24dp)
toolbar.setNavigationOnClickListener { finishActivity(false) }
}
@SuppressLint("SetJavaScriptEnabled")
private fun setupWebView() {
web_view.settings.javaScriptEnabled = true
web_view.webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
// The user can be already logged in the CAS, so check if the URL contains the "ticket" word
// (that means he/she is successful authenticated and we don't need to wait until the page is finished.
if (url.contains("ticket")) {
finishActivity(true)
}
}
override fun onPageFinished(view: WebView, url: String) {
if (url.contains("ticket")) {
finishActivity(true)
} else {
view_loading.hide()
}
}
}
web_view.loadUrl(webPageUrl)
}
private fun finishActivity(setResultOk: Boolean) {
if (setResultOk) {
setResult(Activity.RESULT_OK, Intent().putExtra(INTENT_CAS_TOKEN, casToken))
finish()
} else {
super.onBackPressed()
}
overridePendingTransition(R.anim.hold, R.anim.slide_down)
}
}
\ No newline at end of file
package chat.rocket.android.webview
package chat.rocket.android.webview.ui
import android.annotation.SuppressLint
import android.content.Context
......@@ -19,6 +19,7 @@ fun Context.webViewIntent(webPageUrl: String): Intent {
private const val INTENT_WEB_PAGE_URL = "web_page_url"
// Simple WebView to load URL.
class WebViewActivity : AppCompatActivity() {
private lateinit var webPageUrl: String
......
......@@ -14,7 +14,7 @@ import android.support.v7.widget.RecyclerView
class DividerItemDecoration() : RecyclerView.ItemDecoration() {
private lateinit var divider: Drawable
private var boundStart = 0
private var boundRight = 0
private var boundEnd = 0
// Default divider will be used.
constructor(context: Context) : this() {
......@@ -24,15 +24,15 @@ class DividerItemDecoration() : RecyclerView.ItemDecoration() {
styledAttributes.recycle()
}
// Default divider with custom boundaries (start and right) will be used.
constructor(context: Context, boundStart: Int, boundRight: Int) : this() {
// Default divider with custom boundaries (start and end) will be used.
constructor(context: Context, boundStart: Int, boundEnd: Int) : this() {
val attrs = intArrayOf(android.R.attr.listDivider)
val styledAttributes = context.obtainStyledAttributes(attrs)
divider = styledAttributes.getDrawable(0)
styledAttributes.recycle()
this.boundStart = boundStart
this.boundRight = boundRight
this.boundEnd = boundEnd
}
// Custom divider will be used.
......@@ -45,7 +45,7 @@ class DividerItemDecoration() : RecyclerView.ItemDecoration() {
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val left = parent.paddingLeft + boundStart
val right = (parent.width - parent.paddingRight) - boundRight
val right = (parent.width - parent.paddingRight) - boundEnd
val childCount = parent.childCount
for (i in 0 until childCount) {
......
package chat.rocket.android.widget.autocompletion.model
abstract class SuggestionModel(val text: String, // This is the text key for searches, must be unique.
val searchList: List<String> = emptyList(), // Where to search for matches.
val pinned: Boolean = false /* If pinned item will have priority to show */) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SuggestionModel) return false
if (text != other.text) return false
return true
}
override fun hashCode(): Int {
return text.hashCode()
}
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.repository
interface LocalSuggestionProvider {
fun find(prefix: String)
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.strategy
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
interface CompletionStrategy {
fun getItem(prefix: String, position: Int): SuggestionModel
fun autocompleteItems(prefix: String): List<SuggestionModel>
fun addAll(list: List<SuggestionModel>)
fun size(): Int
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.strategy.regex
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.strategy.CompletionStrategy
import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter
import java.util.concurrent.CopyOnWriteArrayList
internal class StringMatchingCompletionStrategy(private val threshold: Int = -1) : CompletionStrategy {
private val list = CopyOnWriteArrayList<SuggestionModel>()
override fun autocompleteItems(prefix: String): List<SuggestionModel> {
val result = list.filter {
it.searchList.forEach { word ->
if (word.contains(prefix, ignoreCase = true)) {
return@filter true
}
}
false
}.sortedByDescending { it.pinned }
return if (threshold == SuggestionsAdapter.UNLIMITED_RESULT_COUNT) result else result.take(threshold)
}
override fun addAll(list: List<SuggestionModel>) {
this.list.addAllAbsent(list)
}
override fun getItem(prefix: String, position: Int): SuggestionModel {
return list[position]
}
override fun size(): Int {
return list.size
}
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.strategy.trie
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.strategy.CompletionStrategy
import chat.rocket.android.widget.autocompletion.strategy.trie.data.Trie
class TrieCompletionStrategy : CompletionStrategy {
private val items = mutableListOf<SuggestionModel>()
private val trie = Trie()
override fun getItem(prefix: String, position: Int): SuggestionModel {
val item: SuggestionModel
if (prefix.isEmpty()) {
item = items[position]
} else {
item = autocompleteItems(prefix)[position]
}
return item
}
override fun autocompleteItems(prefix: String) = trie.autocompleteItems(prefix)
override fun addAll(list: List<SuggestionModel>) {
items.addAll(list)
list.forEach {
trie.insert(it)
}
}
override fun size() = items.size
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.strategy.trie.data
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
internal class Trie {
private val root = TrieNode(' ')
private var count = 0
fun insert(item: SuggestionModel) {
val sanitizedWord = item.text.trim().toLowerCase()
// Word exists, bail out.
if (search(sanitizedWord)) return
var current = root
sanitizedWord.forEach { ch ->
val child = current.getChild(ch)
if (child == null) {
val node = TrieNode(ch, current)
current.children[ch] = node
current = node
count++
} else {
current = child
}
}
// Set last node as leaf.
if (current != root) {
current.isLeaf = true
current.item = item
}
}
fun search(word: String): Boolean {
val sanitizedWord = word.trim().toLowerCase()
var current = root
sanitizedWord.forEach { ch ->
val child = current.getChild(ch)
if (child == null) {
return false
}
current = child
}
if (current.isLeaf) {
return true
}
return false
}
fun autocomplete(prefix: String): List<String> {
val sanitizedPrefix = prefix.trim().toLowerCase()
var lastNode: TrieNode? = root
sanitizedPrefix.forEach { ch ->
lastNode = lastNode?.getChild(ch)
if (lastNode == null) return emptyList()
}
return lastNode!!.getWords()
}
fun autocompleteItems(prefix: String): List<SuggestionModel> {
val sanitizedPrefix = prefix.trim().toLowerCase()
var lastNode: TrieNode? = root
sanitizedPrefix.forEach { ch ->
lastNode = lastNode?.getChild(ch)
if (lastNode == null) return emptyList()
}
return lastNode!!.getItems()
}
fun getCount() = count
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.strategy.trie.data
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
internal class TrieNode(internal var data: Char,
internal var parent: TrieNode? = null,
internal var isLeaf: Boolean = false,
internal var item: SuggestionModel? = null) {
val children = hashMapOf<Char, TrieNode>()
fun getChild(c: Char): TrieNode? {
children.forEach {
if (it.key == c) return it.value
}
return null
}
fun getWords(): List<String> {
val list = arrayListOf<String>()
if (isLeaf) {
list.add(toString())
}
children.forEach { node ->
node.value.let {
list.addAll(it.getWords())
}
}
return list
}
class X : SuggestionModel("")
fun getItems(): List<SuggestionModel> {
val list = arrayListOf<SuggestionModel>()
if (isLeaf) {
list.add(item!!)
}
children.forEach { node ->
node.value.let {
list.addAll(it.getItems())
}
}
return list
}
override fun toString(): String = if (parent == null) "" else "${parent.toString()}$data"
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.ui
import android.support.v7.widget.RecyclerView
import android.view.View
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
abstract class BaseSuggestionViewHolder(view: View) : RecyclerView.ViewHolder(view) {
abstract fun bind(item: SuggestionModel, itemClickListener: SuggestionsAdapter.ItemClickListener?)
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.ui
import android.content.Context
import android.support.v7.widget.RecyclerView
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.view.WindowManager
import chat.rocket.android.R
internal class PopupRecyclerView : RecyclerView {
private var displayWidth: Int = 0
constructor(context: Context?) : this(context, null)
constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) {
val wm = context!!.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val display = wm.defaultDisplay
val size = DisplayMetrics()
display.getMetrics(size)
val screenWidth = size.widthPixels
displayWidth = screenWidth
}
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
val hSpec = MeasureSpec.makeMeasureSpec(resources.getDimensionPixelSize(
R.dimen.popup_max_height), MeasureSpec.AT_MOST)
val wSpec = MeasureSpec.makeMeasureSpec(displayWidth, MeasureSpec.EXACTLY)
super.onMeasure(wSpec, hSpec)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
super.onLayout(changed, l + 40, t, r - 40, b)
}
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.ui
import android.support.v7.widget.RecyclerView
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.strategy.CompletionStrategy
import chat.rocket.android.widget.autocompletion.strategy.regex.StringMatchingCompletionStrategy
import java.lang.reflect.Type
import kotlin.properties.Delegates
abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>(
val token: String,
val constraint: Int = CONSTRAINT_UNBOUND,
threshold: Int = MAX_RESULT_COUNT) : RecyclerView.Adapter<VH>() {
companion object {
// Any number of results.
const val UNLIMITED_RESULT_COUNT = -1
// Trigger suggestions only if on the line start.
const val CONSTRAINT_BOUND_TO_START = 0
// Trigger suggestions from anywhere.
const val CONSTRAINT_UNBOUND = 1
// Maximum number of results to display by default.
private const val MAX_RESULT_COUNT = 5
}
private var itemType: Type? = null
private var itemClickListener: ItemClickListener? = null
// Called to gather results when no results have previously matched.
private var providerExternal: ((query: String) -> Unit)? = null
// Maximum number of results/suggestions to display.
private var resultsThreshold: Int = if (threshold > 0) threshold else UNLIMITED_RESULT_COUNT
// The strategy used for suggesting completions.
private val strategy: CompletionStrategy = StringMatchingCompletionStrategy(resultsThreshold)
// Current input term to look up for suggestions.
private var currentTerm: String by Delegates.observable("", { _, _, newTerm ->
val items = strategy.autocompleteItems(newTerm)
notifyDataSetChanged()
})
init {
setHasStableIds(true)
}
override fun getItemId(position: Int): Long {
return getItem(position).text.hashCode().toLong()
}
override fun onBindViewHolder(holder: VH, position: Int) {
holder.bind(getItem(position), itemClickListener)
}
override fun getItemCount() = strategy.autocompleteItems(currentTerm).size
private fun getItem(position: Int): SuggestionModel {
return strategy.autocompleteItems(currentTerm)[position]
}
fun autocomplete(newTerm: String) {
this.currentTerm = newTerm.toLowerCase().trim()
}
fun addItems(list: List<SuggestionModel>) {
strategy.addAll(list)
// Since we've just added new items we should check for possible new completion suggestions.
strategy.autocompleteItems(currentTerm)
notifyDataSetChanged()
}
fun setOnClickListener(clickListener: ItemClickListener) {
this.itemClickListener = clickListener
}
fun hasItemClickListener() = itemClickListener != null
/**
* Return the current searched term.
*/
fun term() = this.currentTerm
/**
* Set the maximum number of results to show.
*
* @param threshold The maximum number of suggestions to display.
*/
fun setResultsThreshold(threshold: Int) {
check(threshold > 0)
resultsThreshold = threshold
}
fun cancel() {
strategy.addAll(emptyList())
strategy.autocompleteItems(currentTerm)
notifyDataSetChanged()
}
interface ItemClickListener {
fun onClick(item: SuggestionModel)
}
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.ui
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.support.annotation.DrawableRes
import android.support.transition.Slide
import android.support.transition.TransitionManager
import android.support.v4.content.ContextCompat
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.text.Editable
import android.text.InputType
import android.text.TextWatcher
import android.util.AttributeSet
import android.view.Gravity
import android.view.View
import android.widget.EditText
import android.widget.FrameLayout
import chat.rocket.android.R
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter.Companion.CONSTRAINT_BOUND_TO_START
import java.lang.ref.WeakReference
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicInteger
/**
* This is a special index that means we're not at an autocompleting state.
*/
private const val NO_STATE_INDEX = 0
class SuggestionsView : FrameLayout, TextWatcher {
private val recyclerView: RecyclerView
private val registeredTokens = CopyOnWriteArrayList<String>()
// Maps tokens to their respective adapters.
private val adaptersByToken = hashMapOf<String, SuggestionsAdapter<out BaseSuggestionViewHolder>>()
private val externalProvidersByToken = hashMapOf<String, ((query: String) -> Unit)>()
private val localProvidersByToken = hashMapOf<String, HashMap<String, List<SuggestionModel>>>()
private var editor: WeakReference<EditText>? = null
private var completionStartIndex = AtomicInteger(NO_STATE_INDEX)
private var maxHeight: Int = 0
companion object {
private val SLIDE_TRANSITION = Slide(Gravity.BOTTOM).setDuration(200)
}
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr, 0) {
recyclerView = RecyclerView(context)
val layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL,
false)
recyclerView.itemAnimator = DefaultItemAnimator()
recyclerView.addItemDecoration(TopItemDecoration(context, R.drawable.suggestions_menu_decorator))
recyclerView.layoutManager = layoutManager
recyclerView.visibility = View.GONE
addView(recyclerView)
}
override fun afterTextChanged(s: Editable) {
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
// If we have a deletion.
if (after == 0) {
val deleted = s.subSequence(start, start + count).toString()
if (adaptersByToken.containsKey(deleted) && completionStartIndex.get() > NO_STATE_INDEX) {
// We have removed the '@', '#' or any other action token so halt completion.
cancelSuggestions(true)
}
}
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
// If we don't have any adapter bound to any token bail out.
if (adaptersByToken.isEmpty()) return
val new = s.subSequence(start, start + count).toString()
if (adaptersByToken.containsKey(new)) {
val constraint = adapter(new).constraint
if (constraint == CONSTRAINT_BOUND_TO_START && start != 0) {
return
}
swapAdapter(getAdapterForToken(new)!!)
completionStartIndex.compareAndSet(NO_STATE_INDEX, start + 1)
editor?.let {
// Disable keyboard suggestions when autocompleting.
val editText = it.get()
if (editText != null) {
editText.inputType = editText.inputType or InputType.TYPE_TEXT_VARIATION_FILTER
expand()
}
}
}
if (new.startsWith(" ")) {
// just halts the completion execution
cancelSuggestions(false)
return
}
val prefixEndIndex = editor?.get()?.selectionStart ?: NO_STATE_INDEX
if (prefixEndIndex == NO_STATE_INDEX || prefixEndIndex < completionStartIndex.get()) return
val prefix = s.subSequence(completionStartIndex.get(), editor?.get()?.selectionStart ?: completionStartIndex.get()).toString()
recyclerView.adapter?.let {
it as SuggestionsAdapter
// we need to look up only after the '@'
it.autocomplete(prefix)
val cacheMap = localProvidersByToken[it.token]
if (cacheMap != null && cacheMap[prefix] != null) {
it.addItems(cacheMap[prefix]!!)
} else {
// fetch more suggestions from an external source if any
externalProvidersByToken[it.token]?.invoke(prefix)
}
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
if (maxHeight > 0) {
val hSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST)
super.onMeasure(widthMeasureSpec, hSpec)
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
}
private fun swapAdapter(adapter: SuggestionsAdapter<*>): SuggestionsView {
recyclerView.adapter = adapter
// Don't override if user has set an item click listener already
if (!adapter.hasItemClickListener()) {
setOnItemClickListener(adapter) {
// set default item click behavior
}
}
return this
}
private fun getAdapterForToken(token: String): SuggestionsAdapter<*>? = adaptersByToken.get(token)
fun anchorTo(editText: EditText): SuggestionsView {
editText.removeTextChangedListener(this)
editText.addTextChangedListener(this)
editor = WeakReference(editText)
return this
}
fun addTokenAdapter(adapter: SuggestionsAdapter<*>): SuggestionsView {
adaptersByToken.getOrPut(adapter.token, { adapter })
return this
}
fun addItems(token: String, list: List<SuggestionModel>): SuggestionsView {
if (list.isNotEmpty()) {
val adapter = adapter(token)
localProvidersByToken.getOrPut(token, { hashMapOf() })
.put(adapter.term(), list)
if (completionStartIndex.get() > NO_STATE_INDEX && adapter.itemCount == 0) expand()
adapter.addItems(list)
}
return this
}
fun setMaximumHeight(height: Int): SuggestionsView {
check(height > 0)
this.maxHeight = height
requestLayout()
return this
}
fun setOnItemClickListener(tokenAdapter: SuggestionsAdapter<*>,
clickListener: (item: SuggestionModel) -> Unit): SuggestionsView {
tokenAdapter.setOnClickListener(object : SuggestionsAdapter.ItemClickListener {
override fun onClick(item: SuggestionModel) {
insertSuggestionOnEditor(item)
clickListener.invoke(item)
cancelSuggestions(true)
collapse()
}
})
return this
}
fun addSuggestionProviderAction(token: String, provider: (query: String) -> Unit): SuggestionsView {
if (adaptersByToken[token] == null) {
throw IllegalStateException("token \"$token\" suggestion provider added without adapter")
}
externalProvidersByToken.getOrPut(token, { provider })
return this
}
private fun adapter(token: String): SuggestionsAdapter<*> {
return adaptersByToken[token] ?: throw IllegalStateException("no adapter binds to token \"$token\"")
}
private fun cancelSuggestions(haltCompletion: Boolean) {
// Reset completion start index only if we've deleted the token that triggered completion or
// we finished the completion process.
if (haltCompletion) {
completionStartIndex.set(NO_STATE_INDEX)
}
collapse()
// Re-enable keyboard suggestions.
val editText = editor?.get()
if (editText != null) {
editText.inputType = editText.inputType and InputType.TYPE_TEXT_VARIATION_FILTER.inv()
}
}
private fun insertSuggestionOnEditor(item: SuggestionModel) {
editor?.get()?.let {
val suggestionText = item.text
it.text.replace(completionStartIndex.get(), it.selectionStart, "$suggestionText ")
}
}
private fun collapse() {
TransitionManager.beginDelayedTransition(this, SLIDE_TRANSITION)
recyclerView.visibility = View.GONE
}
private fun expand() {
TransitionManager.beginDelayedTransition(this, SLIDE_TRANSITION)
recyclerView.visibility = View.VISIBLE
}
private class TopItemDecoration() : RecyclerView.ItemDecoration() {
private lateinit var divider: Drawable
private val padding = Rect()
// Custom divider will be used.
constructor(context: Context, @DrawableRes drawableResId: Int) : this() {
val customDrawable = ContextCompat.getDrawable(context, drawableResId)
if (customDrawable != null) {
divider = customDrawable
}
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val left = parent.paddingLeft
val right = (parent.width - parent.paddingRight)
val parentParams = parent.layoutParams as FrameLayout.LayoutParams
val top = parent.top - parentParams.topMargin - parent.paddingTop
val bottom = top + divider.intrinsicHeight
divider.setBounds(left, top, right, bottom)
divider.draw(c)
}
}
}
\ No newline at end of file
package chat.rocket.android.widget.emoji
import android.support.v4.view.PagerAdapter
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.GridLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import chat.rocket.android.R
import java.util.*
class CategoryPagerAdapter(val listener: EmojiKeyboardListener) : PagerAdapter() {
override fun isViewFromObject(view: View, obj: Any): Boolean {
return view == obj
}
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val view = LayoutInflater.from(container.context)
.inflate(R.layout.emoji_category_layout, container, false)
val layoutManager = GridLayoutManager(view.context, 8)
val recycler = view.findViewById(R.id.emojiRecyclerView) as RecyclerView
val adapter = EmojiAdapter(layoutManager.spanCount, listener)
val category = EmojiCategory.values().get(position)
val emojis = if (category != EmojiCategory.RECENTS) {
EmojiRepository.getEmojisByCategory(category)
} else {
EmojiRepository.getRecents()
}
adapter.addEmojis(emojis)
recycler.layoutManager = layoutManager
recycler.itemAnimator = DefaultItemAnimator()
recycler.adapter = adapter
recycler.isNestedScrollingEnabled = false
container.addView(view)
return view
}
override fun destroyItem(container: ViewGroup, position: Int, view: Any) {
container.removeView(view as View)
}
override fun getCount() = EmojiCategory.values().size
override fun getPageTitle(position: Int) = EmojiCategory.values()[position].textIcon()
class EmojiAdapter(val spanCount: Int, val listener: EmojiKeyboardListener) : RecyclerView.Adapter<EmojiRowViewHolder>() {
private var emojis = Collections.emptyList<Emoji>()
fun addEmojis(emojis: List<Emoji>) {
this.emojis = emojis
notifyItemRangeInserted(0, emojis.size)
}
override fun onBindViewHolder(holder: EmojiRowViewHolder, position: Int) {
holder.bind(emojis[position])
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmojiRowViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.emoji_row_item, parent, false)
return EmojiRowViewHolder(view, itemCount, spanCount, listener)
}
override fun getItemCount(): Int = emojis.size
}
class EmojiRowViewHolder(itemView: View, val itemCount: Int, val spanCount: Int, val listener: EmojiKeyboardListener) : RecyclerView.ViewHolder(itemView) {
private val emojiView: TextView = itemView.findViewById(R.id.emoji)
fun bind(emoji: Emoji) {
val context = itemView.context
emojiView.text = EmojiParser.parse(emoji.unicode)
val remainder = itemCount % spanCount
val lastLineItemCount = if (remainder == 0) spanCount else remainder
val paddingBottom = context.resources.getDimensionPixelSize(R.dimen.picker_padding_bottom)
if (adapterPosition >= itemCount - lastLineItemCount) {
itemView.setPadding(0, 0, 0, paddingBottom)
}
itemView.setOnClickListener {
listener.onEmojiAdded(emoji)
}
}
}
}
\ No newline at end of file
package chat.rocket.android.widget.emoji
import android.content.Context
import android.support.v7.widget.AppCompatEditText
import android.util.AttributeSet
import android.view.KeyEvent
class ComposerEditText : AppCompatEditText {
var listener: ComposerEditTextListener? = null
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
super(context, attrs, defStyleAttr) {
isFocusable = true
isFocusableInTouchMode = true
isClickable = true
isLongClickable = true
}
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, android.support.v7.appcompat.R.attr.editTextStyle)
constructor(context: Context) : this(context, null)
override fun dispatchKeyEventPreIme(event: KeyEvent): Boolean {
if (event.keyCode == KeyEvent.KEYCODE_BACK) {
val state = getKeyDispatcherState()
if (state != null) {
if (event.action == KeyEvent.ACTION_DOWN) {
state.startTracking(event, this)
listener?.onKeyboardClosed()
}
return true
}
}
return super.dispatchKeyEventPreIme(event)
}
override fun performClick(): Boolean {
listener?.onKeyboardOpened()
return super.performClick()
}
interface ComposerEditTextListener {
fun onKeyboardClosed()
fun onKeyboardOpened()
}
}
\ No newline at end of file
package chat.rocket.android.widget.emoji
data class Emoji(
val shortname: String,
val shortnameAlternates: List<String>,
val unicode: String,
val keywords: List<String>,
val category: String,
val count: Int = 0
)
\ No newline at end of file
package chat.rocket.android.widget.emoji
import android.support.annotation.DrawableRes
import android.text.SpannableString
import android.text.Spanned
import chat.rocket.android.R
enum class EmojiCategory {
RECENTS {
override fun resourceIcon() = R.drawable.ic_emoji_recents
override fun textIcon() = getTextIconFor("\uD83D\uDD58")
},
PEOPLE() {
override fun resourceIcon() = R.drawable.ic_emoji_people
override fun textIcon() = getTextIconFor("\uD83D\uDE00")
},
NATURE {
override fun resourceIcon() = R.drawable.ic_emoji_nature
override fun textIcon() = getTextIconFor("\uD83D\uDC3B")
},
FOOD {
override fun resourceIcon() = R.drawable.ic_emoji_food
override fun textIcon() = getTextIconFor("\uD83C\uDF4E")
},
ACTIVITY {
override fun resourceIcon() = R.drawable.ic_emoji_activity
override fun textIcon() = getTextIconFor("\uD83D\uDEB4")
},
TRAVEL {
override fun resourceIcon() = R.drawable.ic_emoji_travel
override fun textIcon() = getTextIconFor("\uD83C\uDFD9️")
},
OBJECTS {
override fun resourceIcon() = R.drawable.ic_emoji_objects
override fun textIcon() = getTextIconFor("\uD83D\uDD2A")
},
SYMBOLS {
override fun resourceIcon() = R.drawable.ic_emoji_symbols
override fun textIcon() = getTextIconFor("⚛")
},
FLAGS {
override fun resourceIcon() = R.drawable.ic_emoji_flags
override fun textIcon() = getTextIconFor("\uD83D\uDEA9")
};
abstract fun textIcon(): CharSequence
@DrawableRes
abstract fun resourceIcon(): Int
protected fun getTextIconFor(text: String): CharSequence {
val span = EmojiTypefaceSpan("sans-serif", EmojiRepository.cachedTypeface)
return SpannableString.valueOf(text).apply {
setSpan(span, 0, text.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
}
}
}
\ No newline at end of file
package chat.rocket.android.widget.emoji
interface EmojiKeyboardListener {
/**
* When an emoji is selected on the picker.
*
* @param emoji The selected emoji
*/
fun onEmojiAdded(emoji: Emoji)
/**
* When backspace key is clicked.
*
* @param keyCode The key code pressed as defined
*
* @see android.view.KeyEvent
*/
fun onNonEmojiKeyPressed(keyCode: Int)
}
\ No newline at end of file
package chat.rocket.android.widget.emoji
import android.content.Context
import android.support.design.widget.TabLayout
import android.support.v4.view.ViewPager
import android.support.v7.app.AppCompatActivity
import android.text.Editable
import android.text.TextWatcher
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView
import chat.rocket.android.R
class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow(context, view) {
private lateinit var viewPager: ViewPager
private lateinit var tabLayout: TabLayout
private lateinit var searchView: View
private lateinit var backspaceView: View
private lateinit var parentContainer: ViewGroup
var listener: EmojiKeyboardListener? = null
companion object {
const val PREF_EMOJI_RECENTS = "PREF_EMOJI_RECENTS"
}
override fun onCreateView(inflater: LayoutInflater): View {
val view = inflater.inflate(R.layout.emoji_keyboard, null)
parentContainer = view.findViewById(R.id.emoji_keyboard_container)
viewPager = view.findViewById(R.id.pager_categories)
searchView = view.findViewById(R.id.emoji_search)
backspaceView = view.findViewById(R.id.emoji_backspace)
tabLayout = view.findViewById(R.id.tabs)
tabLayout.setupWithViewPager(viewPager)
return view
}
override fun onViewCreated(view: View) {
setupViewPager()
setupBottomBar()
}
private fun setupBottomBar() {
searchView.setOnClickListener {
}
backspaceView.setOnClickListener {
listener?.onNonEmojiKeyPressed(KeyEvent.KEYCODE_BACK)
}
}
private fun setupViewPager() {
context.let {
val callback = when (it) {
is EmojiKeyboardListener -> it
else -> {
val fragments = (it as AppCompatActivity).supportFragmentManager.fragments
if (fragments == null || fragments.size == 0 || !(fragments[0] is EmojiKeyboardListener)) {
throw IllegalStateException("activity/fragment should implement Listener interface")
}
fragments[0] as EmojiKeyboardListener
}
}
viewPager.adapter = CategoryPagerAdapter(object : EmojiListenerAdapter() {
override fun onEmojiAdded(emoji: Emoji) {
EmojiRepository.addToRecents(emoji)
callback.onEmojiAdded(emoji)
}
})
for (category in EmojiCategory.values()) {
val tab = tabLayout.getTabAt(category.ordinal)
val tabView = LayoutInflater.from(context).inflate(R.layout.emoji_picker_tab, null)
tab?.customView = tabView
val textView = tabView.findViewById(R.id.image_category) as ImageView
textView.setImageResource(category.resourceIcon())
}
val currentTab = if (EmojiRepository.getRecents().isEmpty()) EmojiCategory.PEOPLE.ordinal else
EmojiCategory.RECENTS.ordinal
viewPager.currentItem = currentTab
}
}
class EmojiTextWatcher(val editor: EditText) : TextWatcher {
@Volatile private var emojiToRemove = mutableListOf<EmojiTypefaceSpan>()
override fun afterTextChanged(s: Editable) {
val message = editor.getEditableText()
// Commit the emoticons to be removed.
for (span in emojiToRemove.toList()) {
val start = message.getSpanStart(span)
val end = message.getSpanEnd(span)
// Remove the span
message.removeSpan(span)
// Remove the remaining emoticon text.
if (start != end) {
message.delete(start, end)
}
break
}
emojiToRemove.clear()
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
if (after < count) {
val end = start + count
val message = editor.getEditableText()
val list = message.getSpans(start, end, EmojiTypefaceSpan::class.java)
for (span in list) {
val spanStart = message.getSpanStart(span)
val spanEnd = message.getSpanEnd(span)
if (spanStart < end && spanEnd > start) {
// Add to remove list
emojiToRemove.add(span)
}
}
}
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
}
}
}
\ No newline at end of file
package chat.rocket.android.widget.emoji
abstract class EmojiListenerAdapter : EmojiKeyboardListener {
override fun onEmojiAdded(emoji: Emoji) {
// this space is for rent
}
override fun onNonEmojiKeyPressed(keyCode: Int) {
// this space is for rent
}
}
\ No newline at end of file
package chat.rocket.android.widget.emoji
import android.text.SpannableString
import android.text.Spanned
class EmojiParser {
companion object {
/**
* Parses a text string containing unicode characters and/or shortnames to a rendered
* Spannable.
*
* @param text The text to parse
* @return A rendered Spannable containing any supported emoji.
*/
fun parse(text: CharSequence): CharSequence {
val unicodedText = EmojiRepository.shortnameToUnicode(text, true)
val spannableString = SpannableString.valueOf(unicodedText)
// Look for groups of emojis, set a CustomTypefaceSpan with the emojione font
val length = spannableString.length
var inEmoji = false
var emojiStart = 0
var offset = 0
while (offset < length) {
val codepoint = unicodedText.codePointAt(offset)
val count = Character.charCount(codepoint)
if (codepoint >= 0x200) {
if (!inEmoji) {
emojiStart = offset
}
inEmoji = true
} else {
if (inEmoji) {
spannableString.setSpan(EmojiTypefaceSpan("sans-serif", EmojiRepository.cachedTypeface),
emojiStart, offset, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
inEmoji = false
}
offset += count
if (offset >= length && inEmoji) {
spannableString.setSpan(EmojiTypefaceSpan("sans-serif", EmojiRepository.cachedTypeface),
emojiStart, offset, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
return spannableString
}
}
}
\ No newline at end of file
package chat.rocket.android.widget.emoji
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.support.design.widget.TabLayout
import android.support.v4.view.ViewPager
import android.view.LayoutInflater
import android.view.Window
import android.view.WindowManager
import android.widget.ImageView
import chat.rocket.android.R
class EmojiPickerPopup(context: Context) : Dialog(context) {
private lateinit var viewPager: ViewPager
private lateinit var tabLayout: TabLayout
var listener: EmojiKeyboardListener? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
setContentView(R.layout.emoji_picker)
viewPager = findViewById(R.id.pager_categories)
tabLayout = findViewById(R.id.tabs)
tabLayout.setupWithViewPager(viewPager)
setupViewPager()
setSize()
}
private fun setSize() {
val lp = WindowManager.LayoutParams()
lp.copyFrom(window.attributes)
val dialogWidth = lp.width
val dialogHeight = context.resources.getDimensionPixelSize(R.dimen.picker_popup_height)
window.setLayout(dialogWidth, dialogHeight)
}
private fun setupViewPager() {
viewPager.adapter = CategoryPagerAdapter(object : EmojiListenerAdapter() {
override fun onEmojiAdded(emoji: Emoji) {
EmojiRepository.addToRecents(emoji)
dismiss()
listener?.onEmojiAdded(emoji)
}
})
for (category in EmojiCategory.values()) {
val tab = tabLayout.getTabAt(category.ordinal)
val tabView = LayoutInflater.from(context).inflate(R.layout.emoji_picker_tab, null)
tab?.customView = tabView
val textView = tabView.findViewById(R.id.image_category) as ImageView
textView.setImageResource(category.resourceIcon())
}
val currentTab = if (EmojiRepository.getRecents().isEmpty()) EmojiCategory.PEOPLE.ordinal else
EmojiCategory.RECENTS.ordinal
viewPager.currentItem = currentTab
}
}
\ No newline at end of file
package chat.rocket.android.widget.emoji
interface EmojiReactionListener {
/**
* Callback when an emoji is picked in respect to message by the given id.
*
* @param messageId The id of the message being reacted.
* @param emoji The emoji used to react.
*/
fun onReactionAdded(messageId: String, emoji: Emoji)
/**
* Callback when an added reaction is touched.
*
* @param messageId The id of the message with the reaction.
* @param emojiShortname The shortname of the emoji (:grin:, :smiley:, etc).
*/
fun onReactionTouched(messageId: String, emojiShortname: String)
}
\ No newline at end of file
package chat.rocket.android.widget.emoji
import android.content.Context
import android.content.SharedPreferences
import android.graphics.Typeface
import android.os.Build
import org.json.JSONArray
import org.json.JSONObject
import java.io.BufferedReader
import java.io.InputStream
import java.io.InputStreamReader
import java.util.*
import java.util.regex.Pattern
object EmojiRepository {
private val shortNameToUnicode = HashMap<String, String>()
private val SHORTNAME_PATTERN = Pattern.compile(":([-+\\w]+):")
private val ALL_EMOJIS = mutableListOf<Emoji>()
private lateinit var preferences: SharedPreferences
internal lateinit var cachedTypeface: Typeface
fun load(context: Context, path: String = "emoji.json") {
preferences = context.getSharedPreferences("emoji", Context.MODE_PRIVATE)
ALL_EMOJIS.clear()
cachedTypeface = Typeface.createFromAsset(context.assets, "fonts/emojione-android.ttf")
val stream = context.assets.open(path)
val emojis = loadEmojis(stream)
emojis.forEach {
val unicodeIntList = mutableListOf<Int>()
it.unicode.split("-").forEach {
val value = it.toInt(16)
if (value >= 0x10000) {
val surrogatePair = calculateSurrogatePairs(value)
unicodeIntList.add(surrogatePair.first)
unicodeIntList.add(surrogatePair.second)
} else {
unicodeIntList.add(value)
}
}
val unicodeIntArray = unicodeIntList.toIntArray()
val unicode = String(unicodeIntArray, 0, unicodeIntArray.size)
ALL_EMOJIS.add(it.copy(unicode = unicode))
shortNameToUnicode.apply {
put(it.shortname, unicode)
it.shortnameAlternates.forEach { alternate -> put(alternate, unicode) }
}
}
}
/**
* Get all loaded emojis as list of Emoji objects.
*
* @return All emojis for all categories.
*/
fun getAll() = ALL_EMOJIS
/**
* Get all emojis for a given category.
*
* @param category Emoji category such as: PEOPLE, NATURE, ETC
*
* @return All emoji from specified category
*/
fun getEmojisByCategory(category: EmojiCategory): List<Emoji> {
return ALL_EMOJIS.filter { it.category.toLowerCase() == category.name.toLowerCase() }
}
/**
* Get the emoji given by a specified shortname. Returns null if can't find any.
*
* @param shortname The emoji shortname to search for
*
* @return Emoji given by shortname or null
*/
fun getEmojiByShortname(shortname: String) = ALL_EMOJIS.firstOrNull { it.shortname == shortname }
/**
* Add an emoji to the Recents category.
*/
fun addToRecents(emoji: Emoji) {
val emojiShortname = emoji.shortname
val recentsJson = JSONObject(preferences.getString(EmojiKeyboardPopup.PREF_EMOJI_RECENTS, "{}"))
if (recentsJson.has(emojiShortname)) {
val useCount = recentsJson.getInt(emojiShortname)
recentsJson.put(emojiShortname, useCount + 1)
} else {
recentsJson.put(emojiShortname, 1)
}
preferences.edit().putString(EmojiKeyboardPopup.PREF_EMOJI_RECENTS, recentsJson.toString()).apply()
}
/**
* Get all recently used emojis ordered by usage count.
*
* @return All recent emojis ordered by usage.
*/
fun getRecents(): List<Emoji> {
val list = mutableListOf<Emoji>()
val recentsJson = JSONObject(preferences.getString(EmojiKeyboardPopup.PREF_EMOJI_RECENTS, "{}"))
for (shortname in recentsJson.keys()) {
val emoji = getEmojiByShortname(shortname)
emoji?.let {
val useCount = recentsJson.getInt(it.shortname)
list.add(it.copy(count = useCount))
}
}
Collections.sort(list, { o1, o2 ->
o2.count - o1.count
})
return list
}
/**
* Replace shortnames to unicode characters.
*/
fun shortnameToUnicode(input: CharSequence, removeIfUnsupported: Boolean): String {
val matcher = SHORTNAME_PATTERN.matcher(input)
val supported = Build.VERSION.SDK_INT >= 16
var result: String = input.toString()
while (matcher.find()) {
val unicode = shortNameToUnicode.get(":${matcher.group(1)}:")
if (unicode == null) {
continue
}
if (supported) {
result = result.replace(":" + matcher.group(1) + ":", unicode)
} else if (!supported && removeIfUnsupported) {
result = result.replace(":" + matcher.group(1) + ":", "")
}
}
return result
}
private fun loadEmojis(stream: InputStream): List<Emoji> {
val emojisJSON = JSONArray(inputStreamToString(stream))
val emojis = ArrayList<Emoji>(emojisJSON.length());
for (i in 0 until emojisJSON.length()) {
val emoji = buildEmojiFromJSON(emojisJSON.getJSONObject(i))
emoji?.let {
emojis.add(it)
}
}
return emojis
}
private fun buildEmojiFromJSON(json: JSONObject): Emoji? {
if (!json.has("shortname") || !json.has("unicode")) {
return null
}
return Emoji(shortname = json.getString("shortname"),
unicode = json.getString("unicode"),
shortnameAlternates = buildStringListFromJsonArray(json.getJSONArray("shortnameAlternates")),
category = json.getString("category"),
keywords = buildStringListFromJsonArray(json.getJSONArray("keywords")))
}
private fun buildStringListFromJsonArray(array: JSONArray): List<String> {
val list = ArrayList<String>(array.length())
for (i in 0..array.length() - 1) {
list.add(array.getString(i))
}
return list
}
private fun inputStreamToString(stream: InputStream): String {
val sb = StringBuilder()
val isr = InputStreamReader(stream, Charsets.UTF_8)
val br = BufferedReader(isr)
var read: String? = br.readLine()
while (read != null) {
sb.append(read)
read = br.readLine()
}
br.close()
return sb.toString()
}
private fun calculateSurrogatePairs(scalar: Int): Pair<Int, Int> {
val temp: Int = (scalar - 0x10000) / 0x400
val s1: Int = Math.floor(temp.toDouble()).toInt() + 0xD800
val s2: Int = ((scalar - 0x10000) % 0x400) + 0xDC00
return Pair(s1, s2)
}
}
\ No newline at end of file
package chat.rocket.android.widget.emoji
import android.graphics.Paint
import android.graphics.Typeface
import android.text.TextPaint
import android.text.style.TypefaceSpan
class EmojiTypefaceSpan(family: String, private val newType: Typeface) : TypefaceSpan(family) {
override fun updateDrawState(ds: TextPaint) {
applyCustomTypeFace(ds, newType)
}
override fun updateMeasureState(paint: TextPaint) {
applyCustomTypeFace(paint, newType)
}
private fun applyCustomTypeFace(paint: Paint, tf: Typeface) {
val oldStyle: Int
val old = paint.getTypeface()
if (old == null) {
oldStyle = 0
} else {
oldStyle = old.getStyle()
}
val fake = oldStyle and tf.style.inv()
if (fake and Typeface.BOLD != 0) {
paint.setFakeBoldText(true)
}
if (fake and Typeface.ITALIC != 0) {
paint.setTextSkewX(-0.25f)
}
paint.setTypeface(tf)
}
}
\ No newline at end of file
/**
* Copyright 2015 YA LLC
*
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package chat.rocket.android.widget.emoji
import android.content.Context
import android.graphics.Point
import android.graphics.Rect
import android.os.Build
import android.view.*
import android.widget.PopupWindow
import chat.rocket.android.BuildConfig
import chat.rocket.android.R
/**
* Base class to create popup window that appears over software keyboard.
*/
abstract class OverKeyboardPopupWindow(val context: Context, private val rootView: View) : PopupWindow(context), ViewTreeObserver.OnGlobalLayoutListener {
/**
* @return keyboard height in pixels
*/
var keyboardHeight = 0
private set
private var pendingOpen = false
/**
* @return Returns true if the soft keyboard is open, false otherwise.
*/
var isKeyboardOpen = false
private set
private var keyboardHideListener: OnKeyboardHideListener? = null
interface OnKeyboardHideListener {
fun onKeyboardHide()
}
init {
setBackgroundDrawable(null)
if (BuildConfig.VERSION_CODE >= Build.VERSION_CODES.LOLLIPOP) {
elevation = 0f
}
val view = onCreateView(LayoutInflater.from(context))
onViewCreated(view)
contentView = view
softInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE
// Default size
setSize(this.context.resources.getDimensionPixelSize(R.dimen.supposed_keyboard_height),
WindowManager.LayoutParams.MATCH_PARENT)
setSizeForSoftKeyboard()
}
fun setKeyboardHideListener(keyboardHideListener: OnKeyboardHideListener) {
this.keyboardHideListener = keyboardHideListener
}
/**
* Manually set the popup window size
*
* @param width Width of the popup
* @param height Height of the popup
*/
fun setSize(width: Int, height: Int) {
setWidth(width)
setHeight(height)
}
/**
* Call this function to resize the emoji popup according to your soft keyboard size
*/
fun setSizeForSoftKeyboard() {
val viewTreeObserver = rootView.viewTreeObserver
viewTreeObserver.addOnGlobalLayoutListener(this)
}
override fun onGlobalLayout() {
val r = Rect()
rootView.getWindowVisibleDisplayFrame(r)
val screenHeight = calculateScreenHeight()
var heightDifference = screenHeight - (r.bottom - r.top)
val resources = context.resources
val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android")
if (resourceId > 0) {
heightDifference -= resources.getDimensionPixelSize(resourceId)
}
if (heightDifference > 100) {
keyboardHeight = heightDifference
setSize(WindowManager.LayoutParams.MATCH_PARENT, keyboardHeight)
isKeyboardOpen = true
if (pendingOpen) {
showAtBottom()
pendingOpen = false
}
} else {
if (isKeyboardOpen && keyboardHideListener != null) {
keyboardHideListener!!.onKeyboardHide()
}
isKeyboardOpen = false
}
}
private fun calculateScreenHeight(): Int {
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val display = wm.getDefaultDisplay()
val size = Point()
display.getSize(size)
return size.y
}
/**
* Use this function to show the popup.
* NOTE: Since, the soft keyboard sizes are variable on different android devices, the
* library needs you to open the soft keyboard at least once before calling this function.
* If that is not possible see showAtBottomPending() function.
*/
fun showAtBottom() {
showAtLocation(rootView, Gravity.BOTTOM, 0, 0)
}
/**
* Use this function when the soft keyboard has not been opened yet. This
* will show the popup after the keyboard is up next time.
* Generally, you will be calling InputMethodManager.showSoftInput function after
* calling this function.
*/
fun showAtBottomPending() {
if (isKeyboardOpen) {
showAtBottom()
} else {
pendingOpen = true
}
}
abstract fun onCreateView(inflater: LayoutInflater): View
abstract fun onViewCreated(view: View)
}
\ No newline at end of file
......@@ -6,13 +6,15 @@
android:fillEnabled="true"
android:shareInterpolator="false"
android:zAdjustment="top">
<alpha
android:duration="250"
android:duration="100"
android:fromAlpha="0.2"
android:interpolator="@anim/accelerate_cubic"
android:toAlpha="1.0" />
<scale
android:duration="250"
android:duration="100"
android:fromXScale="0.9"
android:fromYScale="0.9"
android:interpolator="@anim/accelerate_cubic"
......@@ -20,4 +22,5 @@
android:pivotY="50.0%p"
android:toXScale="1.0"
android:toYScale="1.0" />
</set>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:zAdjustment="top">
<translate
android:duration="250"
android:duration="100"
android:fromXDelta="0.0%p"
android:interpolator="@anim/accelerate_cubic"
android:toXDelta="100.0%p" />
</set>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="800"
android:duration="500"
android:fromYDelta="0.0%p"
android:toYDelta="0.0%p" />
</set>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:zAdjustment="top">
<translate
android:duration="250"
android:duration="100"
android:fromXDelta="100.0%p"
android:interpolator="@anim/decelerate_cubic"
android:toXDelta="0.0%p" />
</set>
\ No newline at end of file
......@@ -6,13 +6,15 @@
android:fillEnabled="true"
android:shareInterpolator="false"
android:zAdjustment="normal">
<alpha
android:duration="250"
android:duration="100"
android:fromAlpha="1.0"
android:interpolator="@anim/decelerate_cubic"
android:toAlpha="0.2" />
<scale
android:duration="250"
android:duration="100"
android:fromXScale="1.0"
android:fromYScale="1.0"
android:interpolator="@anim/decelerate_cubic"
......@@ -20,4 +22,5 @@
android:pivotY="50.0%p"
android:toXScale="0.9"
android:toYScale="0.9" />
</set>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="350"
android:duration="250"
android:fromYDelta="100.0%"
android:toYDelta="0.0%" />
</set>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="20.0"
android:viewportWidth="20.0">
<path
android:fillColor="#868585"
android:fillType="evenOdd"
android:pathData="M12,8m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
<path
android:fillColor="#868585"
android:fillType="evenOdd"
android:pathData="M8,8m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
<path
android:fillType="evenOdd"
android:pathData="M10,3a7,7 0,1 0,0 14,7 7,0 0,0 7,-7M7.172,12.328a4,4 0,0 0,5.656 0"
android:strokeColor="#868585"
android:strokeWidth="1.5" />
<path
android:fillType="evenOdd"
android:pathData="M16.2,1.2v5.2m-2.6,-2.6h5.2"
android:strokeColor="#868585"
android:strokeLineCap="square"
android:strokeWidth="1.5" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF010101"
android:pathData="M20,12l-1.41,-1.41L13,16.17V4h-2v12.17l-5.58,-5.59L4,12l8,8 8,-8z" />
</vector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path android:pathData="M0,0 L24,0 L24,24 L0,24 Z" />
<path
android:fillColor="@color/colorEmojiIcon"
android:pathData="M22,3 L7,3 C6.31,3,5.77,3.35,5.41,3.88 L0,12 L5.41,20.11
C5.77,20.64,6.31,21,7,21 L22,21 C23.1,21,24,20.1,24,19 L24,5 C24,3.9,23.1,3,22,3
Z M19,15.59 L17.59,17 L14,13.41 L10.41,17 L9,15.59 L12.59,12 L9,8.41 L10.41,7
L14,10.59 L17.59,7 L19,8.41 L15.41,12 L19,15.59 Z" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M10.09,15.59L11.5,17l5,-5 -5,-5 -1.41,1.41L12.67,11H3v2h9.67l-2.58,2.59zM19,3H5c-1.11,0 -2,0.9 -2,2v4h2V5h14v14H5v-4H3v4c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z" />
</vector>
\ No newline at end of file
......@@ -19,58 +19,98 @@
android:strokeWidth="1" />
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M121.13,18.01L119.97,21.57L117.68,28.64C117.56,29 117.04,29 116.92,28.64L114.63,21.57L107,21.57L104.71,28.64C104.59,29 104.08,29 103.96,28.64L101.66,21.57L100.5,18.01C100.4,17.68 100.51,17.33 100.79,17.12L110.82,9.84L120.84,17.12C121.12,17.33 121.23,17.68 121.13,18.01"
android:strokeColor="#00000000"
android:fillAlpha="1"
android:fillColor="#ffffff"
android:pathData="M122.13,22.99L120.97,19.43C119.6,15.19 118.83,12.83 118.68,12.36C118.56,12 118.04,12 117.92,12.36C117.77,12.83 117,15.19 115.63,19.43L108,19.43C106.63,15.19 105.86,12.83 105.71,12.36C105.59,12 105.08,12 104.96,12.36C104.81,12.83 104.04,15.19 102.66,19.43C101.96,21.57 101.58,22.75 101.5,22.99C101.4,23.32 101.51,23.67 101.79,23.88C102.46,24.37 105.8,26.79 111.82,31.16C117.83,26.79 121.17,24.37 121.84,23.88C122.12,23.67 122.23,23.32 122.13,22.99" />
<path
android:fillAlpha="0"
android:fillColor="#FF000000"
android:pathData="M122.13,22.99L120.97,19.43C119.6,15.19 118.83,12.83 118.68,12.36C118.56,12 118.04,12 117.92,12.36C117.77,12.83 117,15.19 115.63,19.43L108,19.43C106.63,15.19 105.86,12.83 105.71,12.36C105.59,12 105.08,12 104.96,12.36C104.81,12.83 104.04,15.19 102.66,19.43C101.96,21.57 101.58,22.75 101.5,22.99C101.4,23.32 101.51,23.67 101.79,23.88C102.46,24.37 105.8,26.79 111.82,31.16C117.83,26.79 121.17,24.37 121.84,23.88C122.12,23.67 122.23,23.32 122.13,22.99"
android:strokeAlpha="0"
android:strokeColor="#000000"
android:strokeWidth="1" />
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M110.82,9.84l0,0l3.81,11.73l-7.62,0z"
android:strokeColor="#00000000"
android:fillAlpha="1"
android:fillColor="#ffffff"
android:pathData="M111.82,31.16L111.82,31.16L115.63,19.43L108.01,19.43L111.82,31.16Z" />
<path
android:fillAlpha="0"
android:fillColor="#FF000000"
android:pathData="M111.82,31.16L111.82,31.16L115.63,19.43L108.01,19.43L111.82,31.16Z"
android:strokeAlpha="0"
android:strokeColor="#000000"
android:strokeWidth="1" />
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M110.82,9.84l-3.81,11.73l-5.34,0z"
android:strokeColor="#00000000"
android:fillAlpha="1"
android:fillColor="#ffffff"
android:pathData="M111.82,31.16L108.01,19.43L102.67,19.43L111.82,31.16Z" />
<path
android:fillAlpha="0"
android:fillColor="#FF000000"
android:pathData="M111.82,31.16L108.01,19.43L102.67,19.43L111.82,31.16Z"
android:strokeAlpha="0"
android:strokeColor="#000000"
android:strokeWidth="1" />
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M101.66,21.57L101.66,21.57L100.5,18.01C100.4,17.68 100.51,17.33 100.79,17.12L110.82,9.84L101.66,21.57L101.66,21.57Z"
android:strokeColor="#00000000"
android:fillAlpha="1"
android:fillColor="#ffffff"
android:pathData="M102.66,19.43C101.96,21.57 101.58,22.75 101.5,22.99C101.4,23.32 101.51,23.67 101.79,23.88C102.46,24.37 105.8,26.79 111.82,31.16L102.66,19.43L102.66,19.43L102.66,19.43Z" />
<path
android:fillAlpha="0"
android:fillColor="#FF000000"
android:pathData="M102.66,19.43C101.96,21.57 101.58,22.75 101.5,22.99C101.4,23.32 101.51,23.67 101.79,23.88C102.46,24.37 105.8,26.79 111.82,31.16L102.66,19.43L102.66,19.43L102.66,19.43Z"
android:strokeAlpha="0"
android:strokeColor="#000000"
android:strokeWidth="1" />
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M101.66,21.57L107,21.57L104.71,28.64C104.59,29 104.08,29 103.96,28.64L101.66,21.57L101.66,21.57Z"
android:strokeColor="#00000000"
android:fillAlpha="1"
android:fillColor="#ffffff"
android:pathData="M108,19.43C106.63,15.19 105.86,12.83 105.71,12.36C105.59,12 105.08,12 104.96,12.36C104.81,12.83 104.04,15.19 102.66,19.43L102.66,19.43L108,19.43Z" />
<path
android:fillAlpha="0"
android:fillColor="#FF000000"
android:pathData="M108,19.43C106.63,15.19 105.86,12.83 105.71,12.36C105.59,12 105.08,12 104.96,12.36C104.81,12.83 104.04,15.19 102.66,19.43L102.66,19.43L108,19.43Z"
android:strokeAlpha="0"
android:strokeColor="#000000"
android:strokeWidth="1" />
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M110.82,9.84l3.81,11.73l5.34,0z"
android:strokeColor="#00000000"
android:fillAlpha="1"
android:fillColor="#ffffff"
android:pathData="M111.82,31.16L115.63,19.43L120.97,19.43L111.82,31.16Z" />
<path
android:fillAlpha="0"
android:fillColor="#FF000000"
android:pathData="M111.82,31.16L115.63,19.43L120.97,19.43L111.82,31.16Z"
android:strokeAlpha="0"
android:strokeColor="#000000"
android:strokeWidth="1" />
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M119.97,21.57L119.97,21.57L121.13,18.01C121.24,17.68 121.12,17.33 120.84,17.12L110.82,9.84L119.97,21.57L119.97,21.57Z"
android:strokeColor="#00000000"
android:fillAlpha="1"
android:fillColor="#ffffff"
android:pathData="M120.97,19.43C121.67,21.57 122.05,22.75 122.13,22.99C122.24,23.32 122.12,23.67 121.84,23.88C121.17,24.37 117.83,26.79 111.82,31.16L120.97,19.43L120.97,19.43L120.97,19.43Z" />
<path
android:fillAlpha="0"
android:fillColor="#FF000000"
android:pathData="M120.97,19.43C121.67,21.57 122.05,22.75 122.13,22.99C122.24,23.32 122.12,23.67 121.84,23.88C121.17,24.37 117.83,26.79 111.82,31.16L120.97,19.43L120.97,19.43L120.97,19.43Z"
android:strokeAlpha="0"
android:strokeColor="#000000"
android:strokeWidth="1" />
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M119.97,21.57L114.63,21.57L116.92,28.64C117.04,29 117.56,29 117.68,28.64L119.97,21.57L119.97,21.57Z"
android:strokeColor="#00000000"
android:fillAlpha="1"
android:fillColor="#ffffff"
android:pathData="M115.63,19.43C117,15.19 117.77,12.83 117.92,12.36C118.04,12 118.56,12 118.68,12.36C118.83,12.83 119.6,15.19 120.97,19.43L120.97,19.43L115.63,19.43Z" />
<path
android:fillAlpha="0"
android:fillColor="#FF000000"
android:pathData="M115.63,19.43C117,15.19 117.77,12.83 117.92,12.36C118.04,12 118.56,12 118.68,12.36C118.83,12.83 119.6,15.19 120.97,19.43L120.97,19.43L115.63,19.43Z"
android:strokeAlpha="0"
android:strokeColor="#000000"
android:strokeWidth="1" />
</vector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M20,5 L4,5 C2.9,5,2.01,5.9,2.01,7 L2,17 C2,18.1,2.9,19,4,19 L20,19
C21.1,19,22,18.1,22,17 L22,7 C22,5.9,21.1,5,20,5 Z M11,8 L13,8 L13,10 L11,10
L11,8 Z M11,11 L13,11 L13,13 L11,13 L11,11 Z M8,8 L10,8 L10,10 L8,10 L8,8 Z
M8,11 L10,11 L10,13 L8,13 L8,11 Z M7,13 L5,13 L5,11 L7,11 L7,13 Z M7,10 L5,10
L5,8 L7,8 L7,10 Z M16,17 L8,17 L8,15 L16,15 L16,17 Z M16,13 L14,13 L14,11 L16,11
L16,13 Z M16,10 L14,10 L14,8 L16,8 L16,10 Z M19,13 L17,13 L17,11 L19,11 L19,13 Z
M19,10 L17,10 L17,8 L19,8 L19,10 Z" />
<path
android:pathData="M0,0 L24,0 L24,24 L0,24 Z M-24,0 L0,0 L0,24 L0,24 Z" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9L7,15.1c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2L8,11v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4L13,17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M22.548,9l0.452,-2h-5.364l1.364,-6h-2l-1.364,6h-5l1.364,-6h-2l-1.364,6h-6.184l-0.452,2h6.182l-1.364,6h-5.36l-0.458,2h5.364l-1.364,6h2l1.364,-6h5l-1.364,6h2l1.364,-6h6.185l0.451,-2h-6.182l1.364,-6h5.366zM13.818,15h-5l1.364,-6h5l-1.364,6z" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportHeight="20.0"
android:viewportWidth="20.0">
<path
android:fillColor="#FF000000"
android:pathData="M13.6,13.47c-0.91,0.953 -2.191,1.545 -3.61,1.545 -2.756,0 -4.99,-2.234 -4.99,-4.99 0,-0.009 0,-0.018 0,-0.026v0.001c0,-2.761 2.239,-5 5,-5 1.131,0 2.175,0.376 3.013,1.009l-0.013,-0.009v-1h2v6.5c0,0.828 0.672,1.5 1.5,1.5s1.5,-0.672 1.5,-1.5v0,-1.5c-0.003,-4.416 -3.584,-7.994 -8,-7.994 -4.418,0 -8,3.582 -8,8s3.582,8 8,8c1.305,0 2.537,-0.312 3.625,-0.867l-0.045,0.021 0.9,1.79c-1.305,0.668 -2.847,1.06 -4.48,1.06 -5.523,0 -10,-4.477 -10,-10s4.477,-10 10,-10c5.519,0 9.994,4.472 10,9.99v0.001h-0.01v1.5c0,0.003 0,0.007 0,0.01 0,1.933 -1.567,3.5 -3.5,3.5 -1.202,0 -2.262,-0.606 -2.892,-1.528l-0.008,-0.012zM10,13c1.657,0 3,-1.343 3,-3s-1.343,-3 -3,-3v0c-1.657,0 -3,1.343 -3,3s1.343,3 3,3v0z" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M18,10v-4c0,-3.313 -2.687,-6 -6,-6s-6,2.687 -6,6v4h-3v14h18v-14h-3zM8,6c0,-2.206 1.794,-4 4,-4s4,1.794 4,4v4h-8v-4zM19,22h-14v-10h14v10z" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="@color/colorEmojiIcon"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
</vector>
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid
android:color="@color/colorBackgroundContentFileAttachment" />
<corners
android:radius="6dp" />
<stroke
android:width="2dp"
android:color="@color/colorDrawableTintGrey" />
<size
android:width="24dp"
android:height="24dp" />
<solid android:color="#efeeee" />
<corners android:radius="4dp"/>
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid
android:color="@color/colorBackgroundFileAttachment" />
<corners
android:radius="6dp" />
<solid android:color="#10000000" />
<corners android:radius="5dp" />
<size android:height="2dp" />
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="10dp"
android:height="10dp"
android:viewportWidth="10.0"
android:viewportHeight="10.0">
<path
android:pathData="M5,5m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
android:fillType="evenOdd"
android:fillColor="#FFFFFF"
android:strokeWidth="1"/>
</vector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:theme="@style/AppTheme"
tools:context=".settings.password.ui.PasswordActivity">
<include
android:id="@+id/layout_app_bar"
layout="@layout/app_bar_password" />
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
\ No newline at end of file
......@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".webview.WebViewActivity">
tools:context=".webview.ui.WebViewActivity">
<include
android:id="@+id/layout_app_bar"
......
......@@ -37,6 +37,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:drawablePadding="@dimen/text_view_drawable_padding"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/white"
......
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.AppBarLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways"
app:navigationIcon="?android:attr/homeAsUpIndicator"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:theme="@style/ActionModeStyle">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/text_change_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold"
tools:text="@string/title_password" />
</RelativeLayout>
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
\ No newline at end of file
......@@ -9,6 +9,6 @@
android:id="@+id/image_avatar"
android:layout_width="40dp"
android:layout_height="40dp"
app:roundedCornerRadius="2dp" />
app:roundedCornerRadius="3dp" />
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.RecyclerView
android:id="@+id/emojiRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.design.widget.CoordinatorLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/emoji_keyboard_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@color/white">
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/colorDividerMessageComposer"
app:layout_constraintBottom_toTopOf="@+id/picker_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<include
android:id="@+id/picker_container"
layout="@layout/emoji_picker"
app:layout_constraintBottom_toTopOf="@+id/emoji_actions_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<RelativeLayout
android:id="@+id/emoji_actions_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@color/whitesmoke"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<ImageView
android:id="@+id/emoji_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:background="@color/whitesmoke"
android:clickable="true"
android:focusable="true"
android:padding="8dp"
android:src="@drawable/ic_search_gray_24px"
android:visibility="invisible" />
<ImageView
android:id="@+id/emoji_backspace"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="@color/whitesmoke"
android:clickable="true"
android:focusable="true"
android:padding="8dp"
android:src="@drawable/ic_backspace_gray_24dp" />
</RelativeLayout>
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/picker_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical">
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabBackground="@color/whitesmoke"
app:tabGravity="fill"
app:tabMaxWidth="48dp"
app:tabMode="scrollable" />
<android.support.v4.view.ViewPager
android:id="@+id/pager_categories"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:background="@color/white" />
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/image_category"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@color/whitesmoke"
android:gravity="center" />
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:foreground="?selectableItemBackground"
android:gravity="center">
<TextView
android:id="@+id/emoji"
style="@style/TextAppearance.AppCompat.Title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:textColor="#000000"
android:textSize="26sp"
tools:text="😀" />
</FrameLayout>
\ No newline at end of file
......@@ -9,13 +9,12 @@
tools:context=".authentication.login.ui.LoginFragment">
<android.support.constraint.ConstraintLayout
android:id="@+id/middle_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/text_headline"
style="@style/HeadlineTextView"
style="@style/Authentication.Headline.TextView"
android:text="@string/title_log_in"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
......@@ -23,7 +22,7 @@
<EditText
android:id="@+id/text_username_or_email"
style="@style/EditText.Authentication"
style="@style/Authentication.EditText"
android:layout_marginTop="32dp"
android:drawableStart="@drawable/ic_assignment_ind_black_24dp"
android:hint="@string/msg_username_or_email"
......@@ -35,7 +34,7 @@
<EditText
android:id="@+id/text_password"
style="@style/EditText.Authentication"
style="@style/Authentication.EditText"
android:layout_marginTop="16dp"
android:drawableStart="@drawable/ic_lock_black_24dp"
android:hint="@string/msg_password"
......@@ -45,6 +44,19 @@
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_username_or_email" />
<Button
android:id="@+id/button_cas"
style="@style/Authentication.Button"
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins"
android:layout_marginStart="@dimen/screen_edge_left_and_right_margins"
android:layout_marginTop="16dp"
android:text="@string/action_login_or_sign_up"
android:visibility="gone"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_password"
tools:visibility="visible" />
<TextView
android:id="@+id/text_new_to_rocket_chat"
android:layout_width="wrap_content"
......@@ -57,7 +69,7 @@
android:visibility="gone"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_password"
app:layout_constraintTop_toBottomOf="@+id/button_cas"
tools:visibility="visible" />
<com.wang.avi.AVLoadingIndicatorView
......@@ -69,7 +81,7 @@
app:indicatorName="BallPulseIndicator"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_password"
app:layout_constraintTop_toBottomOf="@+id/button_cas"
tools:visibility="visible" />
<LinearLayout
......@@ -193,11 +205,10 @@
<Button
android:id="@+id/button_log_in"
style="@style/AuthenticationButton"
style="@style/Authentication.Button"
android:text="@string/title_log_in"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
tools:visibility="gone" />
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" />
</android.support.constraint.ConstraintLayout>
</ScrollView>
\ No newline at end of file
......@@ -10,32 +10,32 @@
<TextView
android:id="@+id/text_headline"
style="@style/HeadlineTextView"
style="@style/Authentication.Headline.TextView"
android:layout_centerHorizontal="true"
android:text="@string/title_sign_in_your_server" />
<TextView
android:id="@+id/text_server_protocol"
style="@style/AuthenticationLabel"
android:layout_below="@id/text_headline"
android:layout_marginTop="32dp"
android:gravity="center_vertical"
android:text="@string/default_protocol" />
<EditText
android:id="@+id/text_server_url"
style="@style/EditText.Authentication"
style="@style/Authentication.EditText"
android:layout_below="@id/text_headline"
android:layout_marginStart="0dp"
android:layout_marginStart="-4dp"
android:layout_marginTop="32dp"
android:layout_toEndOf="@id/text_server_protocol"
android:cursorVisible="false"
android:hint="@string/default_server"
android:imeOptions="actionDone"
android:digits="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-/"
android:digits="0123456789abcdefghijklmnopqrstuvwxyz.-/:"
android:inputType="textUri"
android:paddingEnd="0dp"
android:paddingStart="0dp" />
android:paddingStart="2dp" />
<TextView
android:id="@+id/text_server_protocol"
style="@style/Authentication.TextView"
android:layout_below="@id/text_headline"
android:layout_marginTop="32dp"
android:gravity="center_vertical"
android:text="@string/default_protocol" />
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
......@@ -48,8 +48,8 @@
<Button
android:id="@+id/button_connect"
style="@style/AuthenticationButton"
style="@style/Authentication.Button"
android:layout_alignParentBottom="true"
android:text="@string/action_connect" />
</RelativeLayout>
\ No newline at end of file
</RelativeLayout>
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/constraint_layout"
android:id="@+id/relative_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusableInTouchMode="true"
tools:context=".authentication.signup.ui.SignupFragment">
<TextView
android:id="@+id/text_headline"
style="@style/HeadlineTextView"
android:text="@string/title_sign_up"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/text_name"
style="@style/EditText.Authentication"
android:layout_marginTop="32dp"
android:drawableStart="@drawable/ic_person_black_24dp"
android:hint="@string/msg_name"
android:imeOptions="actionNext"
android:inputType="textCapWords"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_headline" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<EditText
android:id="@+id/text_username"
style="@style/EditText.Authentication"
android:layout_marginTop="16dp"
android:drawableStart="@drawable/ic_at_black_24dp"
android:hint="@string/msg_username"
android:imeOptions="actionNext"
android:inputType="text"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_name" />
<TextView
android:id="@+id/text_headline"
style="@style/Authentication.Headline.TextView"
android:layout_gravity="center"
android:text="@string/title_sign_up" />
<EditText
android:id="@+id/text_password"
style="@style/EditText.Authentication"
android:layout_marginTop="16dp"
android:drawableStart="@drawable/ic_lock_black_24dp"
android:hint="@string/msg_password"
android:imeOptions="actionNext"
android:inputType="textPassword"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_username" />
<EditText
android:id="@+id/text_name"
style="@style/Authentication.EditText"
android:layout_marginTop="32dp"
android:drawableStart="@drawable/ic_person_black_24dp"
android:hint="@string/msg_name"
android:imeOptions="actionNext"
android:inputType="textCapWords" />
<EditText
android:id="@+id/text_email"
style="@style/EditText.Authentication"
android:layout_marginTop="16dp"
android:drawableStart="@drawable/ic_email_black_24dp"
android:hint="@string/msg_email"
android:imeOptions="actionDone"
android:inputType="textEmailAddress"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_password" />
<EditText
android:id="@+id/text_username"
style="@style/Authentication.EditText"
android:layout_marginTop="16dp"
android:drawableStart="@drawable/ic_at_black_24dp"
android:hint="@string/msg_username"
android:imeOptions="actionNext"
android:inputType="text" />
<EditText
android:id="@+id/text_password"
style="@style/Authentication.EditText"
android:layout_marginTop="16dp"
android:drawableStart="@drawable/ic_lock_black_24dp"
android:hint="@string/msg_password"
android:imeOptions="actionNext"
android:inputType="textPassword" />
<EditText
android:id="@+id/text_email"
style="@style/Authentication.EditText"
android:layout_marginBottom="16dp"
android:layout_marginTop="16dp"
android:drawableStart="@drawable/ic_email_black_24dp"
android:hint="@string/msg_email"
android:imeOptions="actionDone"
android:inputType="textEmailAddress" />
</LinearLayout>
</ScrollView>
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_below="@id/scroll_view"
android:layout_centerHorizontal="true"
android:layout_marginTop="16dp"
android:visibility="gone"
app:indicatorName="BallPulseIndicator"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_email"
tools:visibility="visible" />
<TextView
android:id="@+id/text_new_user_agreement"
android:layout_width="wrap_content"
<LinearLayout
android:id="@+id/bottom_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins"
android:layout_marginStart="@dimen/screen_edge_left_and_right_margins"
android:gravity="center"
android:textColorLink="@color/colorAccent"
app:layout_constraintBottom_toTopOf="@+id/button_sign_up"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
android:layout_alignParentBottom="true"
android:orientation="vertical">
<TextView
android:id="@+id/text_new_user_agreement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="@dimen/screen_edge_left_and_right_margins"
android:gravity="center"
android:textColorLink="@color/colorAccent" />
<Button
android:id="@+id/button_sign_up"
style="@style/AuthenticationButton"
android:text="@string/title_sign_up"
app:layout_constraintBottom_toBottomOf="parent" />
<Button
android:id="@+id/button_sign_up"
style="@style/Authentication.Button"
android:text="@string/title_sign_up" />
</LinearLayout>
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
</RelativeLayout>
\ No newline at end of file
......@@ -8,13 +8,13 @@
<TextView
android:id="@+id/text_headline"
style="@style/HeadlineTextView"
style="@style/Authentication.Headline.TextView"
android:layout_centerHorizontal="true"
android:text="@string/title_log_in" />
<EditText
android:id="@+id/text_two_factor_auth"
style="@style/EditText.Authentication"
style="@style/Authentication.EditText"
android:layout_below="@id/text_headline"
android:layout_marginTop="32dp"
android:drawableStart="@drawable/ic_vpn_key_black_24dp"
......@@ -31,10 +31,9 @@
app:indicatorName="BallPulseIndicator"
tools:visibility="visible" />
<Button
android:id="@+id/button_log_in"
style="@style/AuthenticationButton"
style="@style/Authentication.Button"
android:layout_alignParentBottom="true"
android:text="@string/title_log_in" />
......
......@@ -5,7 +5,7 @@
android:id="@+id/root_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
tools:context=".chatroom.ui.ChatRoomFragment">
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
......@@ -20,16 +20,31 @@
<FrameLayout
android:id="@+id/message_list_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:layout_above="@+id/layout_message_composer">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
<include
android:id="@+id/layout_message_list"
layout="@layout/message_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" />
android:layout_height="match_parent" />
</FrameLayout>
<chat.rocket.android.widget.autocompletion.ui.SuggestionsView
android:id="@+id/suggestions_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/layout_message_composer"
android:background="@color/suggestion_background_color" />
<include
android:id="@+id/layout_message_composer"
layout="@layout/message_composer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true" />
<View
android:id="@+id/view_dim"
android:layout_width="match_parent"
......@@ -45,14 +60,21 @@
android:layout_height="wrap_content"
android:layout_above="@+id/layout_message_composer"
android:layout_margin="5dp"
android:visibility="gone"
tools:visibility="visible" />
android:visibility="gone" />
<include
android:id="@+id/layout_message_composer"
layout="@layout/message_composer"
<TextView
android:id="@+id/connection_status_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true" />
android:layout_height="32dp"
android:alpha="0"
android:background="@color/colorPrimary"
android:elevation="4dp"
android:gravity="center"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="@color/white"
android:visibility="gone"
tools:alpha="1"
tools:text="connected"
tools:visibility="visible" />
</RelativeLayout>
......@@ -29,4 +29,19 @@
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/connection_status_text"
android:layout_width="match_parent"
android:layout_height="32dp"
android:background="@color/colorPrimary"
android:elevation="4dp"
android:textColor="@color/white"
android:gravity="center"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:visibility="gone"
android:alpha="0"
tools:alpha="1"
tools:visibility="visible"
tools:text="connected"/>
</RelativeLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/member_bottom_sheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="16dp"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_bottom_sheet_avatar"
android:layout_width="match_parent"
android:layout_height="200dp" />
<LinearLayout
android:id="@+id/name_and_username_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorBackgroundMemberContainer"
android:orientation="vertical"
android:paddingBottom="10dp"
android:paddingStart="16dp"
android:paddingTop="10dp"
app:layout_constraintBottom_toBottomOf="@+id/image_bottom_sheet_avatar"
app:layout_constraintLeft_toLeftOf="parent">
<TextView
android:id="@+id/text_bottom_sheet_member_name"
style="@style/TextAppearance.AppCompat.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
tools:text="Ronald Perkins" />
<TextView
android:id="@+id/text_bottom_sheet_member_username"
style="@style/Sender.Name.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textColor="@color/white"
tools:text="\@ronaldPerkins" />
</LinearLayout>
<TextView
android:id="@+id/text_email_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="@string/msg_email_address"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/image_bottom_sheet_avatar" />
<TextView
android:id="@+id/text_member_email_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="10dp"
android:textColor="@color/colorPrimaryText"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_email_address"
tools:text="ronald@perkins.com" />
<TextView
android:id="@+id/text_utc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="@string/msg_utc_offset"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_member_email_address" />
<TextView
android:id="@+id/text_member_utc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="10dp"
android:textColor="@color/colorPrimaryText"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_utc"
tools:text="+01:00" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".members.ui.MembersFragment">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" />
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_gravity="center"
app:indicatorColor="@color/black"
app:indicatorName="BallPulseIndicator" />
</android.support.design.widget.CoordinatorLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<android.support.design.widget.TextInputLayout
android:id="@+id/layout_new_password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:theme="@style/EditText.Password"
app:layout_constraintBottom_toTopOf="@id/middle_guide"
app:layout_constraintStart_toEndOf="@id/start_guide"
app:layout_constraintEnd_toStartOf="@id/end_guide">
<EditText
android:id="@+id/text_new_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/msg_new_password"
android:inputType="textPassword" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:id="@+id/layout_confirm_password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:theme="@style/EditText.Password"
app:layout_constraintTop_toBottomOf="@id/middle_guide"
app:layout_constraintStart_toEndOf="@id/start_guide"
app:layout_constraintEnd_toStartOf="@id/end_guide">
<EditText
android:id="@+id/text_confirm_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/msg_confirm_password"
android:inputType="textPassword" />
</android.support.design.widget.TextInputLayout>
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/middle_guide"
app:layout_constraintStart_toEndOf="@id/start_guide"
app:layout_constraintEnd_toStartOf="@id/end_guide"
app:indicatorColor="@color/black"
app:indicatorName="BallPulseIndicator" />
<android.support.constraint.Guideline
android:id="@+id/middle_guide"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.2" />
<android.support.constraint.Guideline
android:id="@+id/start_guide"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.05" />
<android.support.constraint.Guideline
android:id="@+id/end_guide"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.95" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
......@@ -25,7 +25,7 @@
<EditText
android:id="@+id/text_name"
style="@style/EditText.Profile"
style="@style/Profile.EditText"
android:layout_marginTop="32dp"
android:drawableStart="@drawable/ic_person_black_24dp"
android:hint="@string/msg_name"
......@@ -33,7 +33,7 @@
<EditText
android:id="@+id/text_username"
style="@style/EditText.Profile"
style="@style/Profile.EditText"
android:layout_marginTop="16dp"
android:drawableStart="@drawable/ic_at_black_24dp"
android:hint="@string/msg_username"
......@@ -41,11 +41,19 @@
<EditText
android:id="@+id/text_email"
style="@style/EditText.Profile"
style="@style/Profile.EditText"
android:layout_marginTop="16dp"
android:drawableStart="@drawable/ic_email_black_24dp"
android:hint="@string/msg_email"
android:inputType="textEmailAddress" />
<EditText
android:id="@+id/text_avatar_url"
style="@style/Profile.EditText"
android:layout_marginTop="16dp"
android:drawableStart="@drawable/ic_link_black_24dp"
android:hint="@string/msg_avatar_url"
android:inputType="text" />
</LinearLayout>
<com.wang.avi.AVLoadingIndicatorView
......
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/settings_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:entries="@array/settings_actions"/>
</RelativeLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="32dp"
android:layout_height="32dp"
android:paddingBottom="2dp"
android:paddingEnd="4dp"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:paddingStart="4dp"
android:paddingTop="2dp"
android:src="@drawable/ic_add_reaction" />
\ No newline at end of file
......@@ -4,111 +4,82 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins"
android:layout_marginStart="@dimen/screen_edge_left_and_right_margins"
android:layout_marginTop="12dp">
android:background="?android:attr/selectableItemBackground"
android:paddingStart="@dimen/screen_edge_left_and_right_padding"
android:paddingEnd="@dimen/screen_edge_left_and_right_padding"
android:paddingTop="@dimen/chat_item_top_and_bottom_padding"
android:paddingBottom="@dimen/chat_item_top_and_bottom_padding">
<android.support.constraint.ConstraintLayout
android:id="@+id/avatar_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
<include
android:id="@+id/layout_avatar"
layout="@layout/avatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="6dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/middle_container"
app:layout_constraintTop_toTopOf="parent">
<include
android:id="@+id/layout_avatar"
layout="@layout/avatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:visibility="gone" />
<!-- We need to build the avatar with initials since the server returns a SVG file with a pre defined 50x50 pixel.
TODO: check to scale the SVG file on the app and remove the view bellow (or make the server to return a jpg file.
SVG scaling with fresco is impossible: http://frescolib.org/docs/resizing.html
-->
<ImageView
android:id="@+id/image_room_avatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:visibility="gone"
tools:ignore="contentDescription" />
</android.support.constraint.ConstraintLayout>
app:layout_constraintTop_toTopOf="parent" />
<android.support.constraint.ConstraintLayout
android:id="@+id/middle_container"
android:id="@+id/top_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
app:layout_constraintLeft_toRightOf="@+id/avatar_container"
app:layout_constraintRight_toLeftOf="@+id/right_container">
app:layout_constraintLeft_toRightOf="@+id/layout_avatar"
app:layout_constraintRight_toRightOf="parent">
<TextView
android:id="@+id/text_chat_name"
style="@style/TextAppearance.AppCompat.Title"
android:layout_width="wrap_content"
style="@style/ChatRoom.Name.TextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:drawablePadding="5dp"
android:ellipsize="end"
android:maxLines="1"
android:drawablePadding="@dimen/text_view_drawable_padding"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/text_last_message_date_time"
app:layout_constraintTop_toTopOf="parent"
tools:text="General" />
<TextView
android:id="@+id/text_last_message"
android:layout_width="0dp"
android:id="@+id/text_last_message_date_time"
style="@style/Timestamp.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
app:layout_constraintTop_toBottomOf="@+id/text_chat_name"
tools:text="You: Type something" />
android:layout_marginStart="5dp"
app:layout_constraintBaseline_toBaselineOf="@+id/text_chat_name"
app:layout_constraintLeft_toRightOf="@+id/text_chat_name"
app:layout_constraintRight_toRightOf="parent"
tools:text="11:45 AM" />
</android.support.constraint.ConstraintLayout>
<android.support.constraint.ConstraintLayout
android:id="@+id/right_container"
android:layout_width="wrap_content"
android:id="@+id/bottom_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintRight_toRightOf="parent">
<TextView
android:id="@+id/text_last_message_date_time"
style="@style/TextAppearance.AppCompat.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/guideline_one"
app:layout_constraintRight_toRightOf="@+id/text_total_unread_messages"
tools:text="11:45" />
android:layout_marginStart="16dp"
android:layout_marginTop="2dp"
app:layout_constraintLeft_toRightOf="@+id/layout_avatar"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/top_container">
<TextView
android:id="@+id/text_total_unread_messages"
style="@style/TextAppearance.AppCompat.Caption"
android:layout_width="20dp"
android:layout_height="20dp"
android:background="@drawable/style_total_unread_messages"
android:gravity="center"
android:textColor="@color/white"
android:textSize="10sp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/guideline_two"
app:layout_constraintRight_toRightOf="parent"
tools:text="99+"
tools:visibility="visible" />
<android.support.constraint.Guideline
android:id="@+id/guideline_one"
android:layout_width="wrap_content"
android:id="@+id/text_last_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_begin="22dp" />
android:ellipsize="end"
android:maxLines="2"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/layout_unread_messages_badge"
app:layout_constraintTop_toTopOf="parent"
tools:text="You: Type something" />
<android.support.constraint.Guideline
android:id="@+id/guideline_two"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_begin="48dp" />
<include
android:id="@+id/layout_unread_messages_badge"
layout="@layout/unread_messages_badge"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginStart="5dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/text_last_message"
app:layout_constraintRight_toRightOf="parent" />
</android.support.constraint.ConstraintLayout>
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:paddingBottom="@dimen/member_item_top_and_bottom_padding"
android:paddingEnd="@dimen/screen_edge_left_and_right_padding"
android:paddingStart="@dimen/screen_edge_left_and_right_padding"
android:paddingTop="@dimen/member_item_top_and_bottom_padding">
<include
android:id="@+id/layout_avatar"
layout="@layout/avatar"
android:layout_width="40dp"
android:layout_height="40dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/text_member"
style="@style/Sender.Name.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
app:layout_constraintBottom_toBottomOf="@+id/layout_avatar"
app:layout_constraintLeft_toRightOf="@+id/layout_avatar"
app:layout_constraintTop_toTopOf="@+id/layout_avatar"
tools:text="Ronald Perkins" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
......@@ -4,19 +4,52 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins"
android:layout_marginStart="@dimen/screen_edge_left_and_right_margins"
android:layout_marginTop="6dp">
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:paddingStart="@dimen/screen_edge_left_and_right_padding"
android:paddingEnd="@dimen/screen_edge_left_and_right_padding"
android:paddingTop="@dimen/message_item_top_and_bottom_padding"
android:paddingBottom="@dimen/message_item_top_and_bottom_padding"
android:focusable="true">
<include
android:id="@+id/layout_avatar"
layout="@layout/avatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="7dp"
android:layout_marginTop="5dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toBottomOf="@id/new_messages_notif" />
<LinearLayout
android:id="@+id/new_messages_notif"
tools:visibility="visible"
android:visibility="gone"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<View
android:layout_gravity="center"
android:layout_height="1dp"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:background="@color/red"/>
<TextView
android:layout_width="wrap_content"
android:text="@string/msg_unread_messages"
android:layout_height="wrap_content"
android:textColor="@color/red" />
<View
android:layout_gravity="center"
android:layout_height="1dp"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:background="@color/red"/>
</LinearLayout>
<LinearLayout
android:id="@+id/top_container"
......@@ -24,46 +57,42 @@
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:orientation="horizontal"
app:layout_constraintTop_toBottomOf="@id/new_messages_notif"
app:layout_constraintLeft_toRightOf="@+id/layout_avatar">
<TextView
android:id="@+id/text_sender"
style="@style/TextAppearance.AppCompat.Title"
style="@style/Sender.Name.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
tools:text="Ronald Perkins" />
<TextView
android:id="@+id/text_message_time"
style="@style/TextAppearance.AppCompat.Caption"
style="@style/Timestamp.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
tools:text="11:45 pm" />
tools:text="11:45 PM" />
</LinearLayout>
<TextView
android:id="@+id/text_content"
style="@style/TextAppearance.AppCompat.Subhead"
style="@style/Message.TextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:layout_marginTop="5dp"
app:layout_constraintLeft_toLeftOf="@id/top_container"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/top_container"
tools:text="This is a multiline chat message from Bertie that will take more than just one line of text. I have sure that everything is amazing!" />
<!-- TODO - Use separate adapter items for messages and attachments. -->
<include
android:id="@+id/message_attachment"
layout="@layout/message_attachment"
<include layout="@layout/layout_reactions"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:layout_constraintLeft_toLeftOf="@id/top_container"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintEnd_toEndOf="@+id/text_content"
app:layout_constraintStart_toStartOf="@+id/text_content"
app:layout_constraintTop_toBottomOf="@+id/text_content" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="2dp"
android:layout_marginRight="2dp"
android:layout_marginTop="2dp"
android:layout_marginBottom="2dp"
android:descendantFocusability="beforeDescendants"
android:background="@drawable/rounded_background"
android:orientation="horizontal">
<TextView
android:id="@+id/text_emoji"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:paddingLeft="4dp"
android:paddingStart="4dp"
android:textColor="#868585"
android:textSize="16sp"
tools:text=":)" />
<TextView
android:id="@+id/text_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingBottom="4dp"
android:paddingEnd="4dp"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:paddingStart="4dp"
android:paddingTop="4dp"
android:textColor="#868585"
android:textSize="16sp"
android:textStyle="bold"
tools:text="12" />
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recycler_view_reactions"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
\ No newline at end of file
......@@ -5,32 +5,18 @@
android:id="@+id/attachment_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/text_file_attachment"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@drawable/style_message_file_upload_content_bg"
android:drawablePadding="10dp"
android:drawableStart="@drawable/ic_files_24dp"
android:ellipsize="end"
android:gravity="center"
android:maxLength="20"
android:maxLines="1"
android:padding="5dp"
android:visibility="gone"
tools:text="brazilian_anthem.mp4"
tools:visibility="visible" />
android:background="?android:attr/selectableItemBackground"
android:orientation="vertical"
android:paddingEnd="@dimen/screen_edge_left_and_right_margins"
android:paddingStart="72dp">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_attachment"
android:layout_width="match_parent"
android:layout_height="150dp"
android:visibility="gone"
fresco:actualImageScaleType="fitStart"
fresco:placeholderImage="@drawable/image_dummy"
tools:visibility="visible" />
android:visibility="visible"
fresco:actualImageScaleType="centerCrop"
fresco:placeholderImage="@drawable/image_dummy" />
<FrameLayout
android:id="@+id/audio_video_attachment"
......@@ -38,7 +24,7 @@
android:layout_height="150dp"
android:background="@color/black"
android:visibility="gone"
tools:visibility="gone">
tools:visibility="visible">
<ImageView
android:layout_width="wrap_content"
......@@ -53,4 +39,9 @@
android:layout_height="wrap_content"
tools:text="Filename.png" />
<include
layout="@layout/layout_reactions"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:orientation="vertical">
<View
android:id="@+id/divider"
<android.support.constraint.ConstraintLayout
android:id="@+id/composer"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/colorDividerMessageComposer" />
android:layout_height="wrap_content">
<TextView
android:id="@+id/text_room_is_read_only"
android:layout_width="match_parent"
android:layout_height="45dp"
android:background="@color/white"
android:gravity="center"
android:text="@string/msg_this_room_is_read_only"
android:textColor="@color/black"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/divider" />
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/colorDividerMessageComposer" />
<LinearLayout
android:id="@+id/input_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:orientation="horizontal"
android:paddingBottom="10dp"
app:layout_constraintTop_toBottomOf="@+id/divider">
<TextView
android:id="@+id/text_room_is_read_only"
android:layout_width="match_parent"
android:layout_height="45dp"
android:background="@color/white"
android:gravity="center"
android:text="@string/msg_this_room_is_read_only"
android:textColor="@color/black"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/divider" />
<ImageButton
android:id="@+id/button_add_reaction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/msg_content_description_show_attachment_options"
android:src="@drawable/ic_reaction_24dp" />
<Button
android:id="@+id/button_join_chat"
android:layout_width="match_parent"
android:layout_height="45dp"
android:background="@color/white"
android:text="@string/action_join_chat"
android:textColor="@color/colorAccent"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/divider" />
<EditText
android:id="@+id/text_message"
android:layout_width="0dp"
<LinearLayout
android:id="@+id/input_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:background="@android:color/transparent"
android:hint="@string/msg_message"
android:maxLines="4" />
android:layout_marginEnd="10dp"
android:layout_marginStart="10dp"
android:orientation="horizontal"
android:paddingBottom="10dp"
android:paddingTop="10dp"
app:layout_constraintTop_toBottomOf="@+id/divider">
<ImageButton
android:id="@+id/button_show_attachment_options"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/msg_content_description_show_attachment_options"
android:src="@drawable/ic_add_24dp" />
<ImageButton
android:id="@+id/button_add_reaction"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="bottom"
android:layout_marginEnd="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/msg_content_description_show_attachment_options"
android:src="@drawable/ic_reaction_24dp" />
<ImageButton
android:id="@+id/button_send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/msg_content_description_send_message"
android:src="@drawable/ic_send_24dp"
android:visibility="gone" />
</LinearLayout>
<chat.rocket.android.widget.emoji.ComposerEditText
android:id="@+id/text_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:background="@android:color/transparent"
android:hint="@string/msg_message"
android:inputType="textCapSentences|textMultiLine"
android:lineSpacingExtra="4dp"
android:maxLines="4"
android:minHeight="24dp"
android:scrollbars="vertical" />
<ImageButton
android:id="@+id/button_show_attachment_options"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="bottom"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/msg_content_description_show_attachment_options"
android:src="@drawable/ic_add_24dp" />
<ImageButton
android:id="@+id/button_send"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="bottom"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/msg_content_description_send_message"
android:src="@drawable/ic_send_24dp"
android:visibility="gone" />
</LinearLayout>
</android.support.constraint.ConstraintLayout>
<FrameLayout
android:id="@+id/emoji_fragment_placeholder"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/button_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="@drawable/ic_arrow_downward_24dp"
android:theme="@style/Theme.AppCompat"
android:tint="@color/gray_material"
android:visibility="invisible"
app:backgroundTint="@color/white"
app:fabSize="mini"
app:layout_anchor="@id/recycler_view"
app:layout_anchorGravity="bottom|end" />
</android.support.design.widget.CoordinatorLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/url_preview_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:paddingEnd="24dp"
android:paddingStart="72dp">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_preview"
android:layout_width="70dp"
android:layout_height="50dp"
app:actualImageScaleType="centerCrop" />
<TextView
android:id="@+id/text_host"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textColor="@color/colorSecondaryText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/image_preview"
tools:text="www.uol.com.br" />
<TextView
android:id="@+id/text_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textColor="@color/colorAccent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/text_host"
app:layout_constraintTop_toBottomOf="@id/text_host"
tools:text="Web page title" />
<TextView
android:id="@+id/text_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/text_host"
app:layout_constraintTop_toBottomOf="@id/text_title"
tools:text="description" />
<include
layout="@layout/layout_reactions"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@+id/image_preview"
app:layout_constraintTop_toBottomOf="@+id/text_description" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginTop="4dp"
android:background="@color/suggestion_background_color"
android:orientation="horizontal"
android:paddingTop="2dp">
<TextView
android:id="@+id/text_command_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginStart="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/black"
android:textSize="14sp"
tools:text="/leave" />
<TextView
android:id="@+id/text_command_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="16dp"
android:layout_toRightOf="@id/text_command_name"
android:ellipsize="end"
android:gravity="start"
android:maxLines="1"
android:textColor="@color/gray_material"
android:textSize="14sp"
tools:text="Leave a channel" />
</RelativeLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:layout_marginEnd="2dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="2dp"
android:layout_marginStart="4dp"
android:layout_marginTop="2dp"
android:background="@color/suggestion_background_color">
<FrameLayout
android:id="@+id/image_avatar_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_avatar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="4dp"
app:roundedCornerRadius="3dp"
tools:src="@tools:sample/avatars" />
<ImageView
android:id="@+id/image_status"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_gravity="bottom|end"
android:background="@drawable/user_status_white"
android:padding="2dp" />
</FrameLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/image_avatar_container"
android:layout_toRightOf="@id/image_avatar_container"
android:background="@color/suggestion_background_color">
<TextView
android:id="@+id/text_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:maxLines="1"
android:textColor="@color/black"
android:textSize="16sp"
tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/text_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_toEndOf="@+id/text_username"
android:layout_toRightOf="@+id/text_username"
android:maxLines="1"
android:paddingLeft="8dp"
android:paddingStart="8dp"
android:textColor="@color/gray_material"
android:textSize="16sp"
tools:text="@tools:sample/full_names" />
</RelativeLayout>
</RelativeLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="2dp"
android:background="@color/suggestion_background_color">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/image_avatar_container"
android:layout_toRightOf="@id/image_avatar_container"
android:background="@color/suggestion_background_color">
<TextView
android:id="@+id/text_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:maxLines="1"
android:textColor="@color/black"
android:textSize="16sp"
tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/text_fullname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_toEndOf="@+id/text_name"
android:layout_toRightOf="@+id/text_name"
android:maxLines="1"
android:paddingLeft="8dp"
android:paddingStart="8dp"
android:textColor="@color/gray_material"
android:textSize="16sp"
tools:text="@tools:sample/full_names" />
</RelativeLayout>
</RelativeLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/text_total_unread_messages"
style="@style/TextAppearance.AppCompat.Caption"
android:layout_width="18dp"
android:layout_height="18dp"
android:background="@drawable/style_total_unread_messages"
android:gravity="center"
android:textColor="@color/white"
android:textSize="10sp"
android:visibility="gone"
tools:text="99+"
tools:visibility="visible" />
</LinearLayout>
\ No newline at end of file
......@@ -2,6 +2,11 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_members_list"
android:title="@string/title_members_list"
app:showAsAction="never" />
<item
android:id="@+id/action_pinned_messages"
android:title="@string/title_pinned_messages"
......
......@@ -9,9 +9,4 @@
app:actionViewClass="android.support.v7.widget.SearchView"
app:showAsAction="ifRoom|collapseActionView" />
<item android:id="@+id/action_logout"
android:title="@string/action_logout"
app:showAsAction="never" />
</menu>
\ No newline at end of file
......@@ -22,9 +22,14 @@
android:icon="@drawable/ic_content_copy_black_24px"
android:title="@string/action_msg_copy" />
<item
android:id="@+id/action_menu_msg_react"
android:icon="@drawable/ic_add_reaction"
android:title="@string/action_msg_add_reaction" />
<!--<item-->
<!--android:id="@+id/action_menu_msg_share"-->
<!--android:icon="@drawable/ic_share_black_24px"-->
<!--andrtextIconicon="@drawable/ic_share_black_24px"-->
<!--android:title="@string/action_msg_share" />-->
<item
......@@ -34,7 +39,7 @@
<!--<item-->
<!--android:id="@+id/action_menu_msg_star"-->
<!--android:icon="@drawable/ic_star_black_24px"-->
<!--andrtextIconicon="@drawable/ic_star_black_24px"-->
<!--android:title="@string/action_msg_star" />-->
</group>
......
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<group
android:id="@+id/menu_section_1"
android:checkableBehavior="single">
<item
android:id="@+id/action_chat_rooms"
android:checked="true"
......@@ -11,6 +15,20 @@
android:id="@+id/action_profile"
android:icon="@drawable/ic_person_black_24dp"
android:title="@string/title_profile" />
<item
android:id="@+id/action_settings"
android:icon="@drawable/ic_settings_black_24dp"
android:title="@string/title_settings"/>
</group>
<group
android:id="@+id/menu_section_2"
android:checkableBehavior="none">
<item
android:id="@+id/action_logout"
android:icon="@drawable/ic_exit_to_app_black_24dp"
android:title="@string/action_logout" />
</group>
</menu>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_password"
android:icon="@drawable/ic_check_white_24dp"
android:title="@string/action_confirm_password" />
</menu>
\ No newline at end of file
......@@ -4,19 +4,33 @@
<string name="title_sign_in_your_server">Faça login no seu servidor</string>
<string name="title_log_in">Entrar</string>
<string name="title_sign_up">Inscreva-se</string>
<string name="title_authentication">Autenticação</string>
<string name="title_legal_terms">Termos Legais</string>
<string name="title_chats">Chats</string>
<string name="title_profile">Perfil</string>
<string name="title_members">Membros (%d)</string>
<string name="title_settings">Configurações</string>
<string name="title_password">Alterar senha</string>
<string name="title_update_profile">Editar perfil</string>
<!-- Actions -->
<string name="action_connect">Conectar</string>
<string name="action_login_or_sign_up">Toque este botão para fazer login ou criar uma conta</string>
<string name="action_terms_of_service">Termos de Serviço</string>
<string name="action_privacy_policy">Política de Privacidade</string>
<string name="action_search">Pesquisar</string>
<string name="action_update">Atualizar</string>
<string name="action_settings">Configurações</string>
<string name="action_logout">Sair</string>
<string name="action_files">Arquivos</string>
<string name="action_confirm_password">Confirme a nova senha</string>
<string name="action_join_chat">Entrar no Chat</string>
<!-- Settings List -->
<string-array name="settings_actions">
<item name="item_password">Alterar senha</item>
</string-array>
<!-- Regular information messages -->
<string name="msg_no_internet_connection">Sem conexão à internet</string>
......@@ -28,6 +42,7 @@
<string name="msg_password">senha</string>
<string name="msg_name">nome</string>
<string name="msg_email">email</string>
<string name="msg_avatar_url">URL do avatar</string>
<string name="msg_or_continue_using_social_accounts">Ou continue através de contas sociais</string>
<string name="msg_new_user">Novo usuário? %1$s</string>
<string name="msg_new_user_agreement">Ao proceder você concorda com nossos %1$s e %2$s</string>
......@@ -49,6 +64,11 @@
<string name="msg_content_description_show_attachment_options">Mostrar opções de anexo</string>
<string name="msg_you">Você</string>
<string name="msg_unknown">Desconhecido</string>
<string name="msg_email_address">Endereço de e-mail</string>
<string name="msg_utc_offset">Deslocamento de UTC</string>
<string name="msg_new_password">Informe a nova senha</string>
<string name="msg_confirm_password">Confirme a nova senha</string>
<string name="msg_unread_messages">Mensagens não lidas</string>
<!-- System messages -->
<string name="message_room_name_changed">Nome da sala alterado para: %1$s por %2$s</string>
......@@ -67,17 +87,55 @@
<string name="action_msg_quote">Citar</string>
<string name="action_msg_delete">Remover</string>
<string name="action_msg_pin">Fixar Mensagem</string>
<string name="action_msg_unpin">Desafixar Messagem</string>
<string name="action_msg_unpin">Desafixar Mensagem</string>
<string name="action_msg_star">Favoritar Mensagem</string>
<string name="action_msg_share">Compartilhar</string>
<string name="action_title_editing">Editando Mensagem</string>
<string name="action_msg_add_reaction">Adicionar reação</string>
<!-- Permission messages -->
<string name="permission_editing_not_allowed">Edição não permitida</string>
<string name="permission_deleting_not_allowed">Remoção não permitida</string>
<string name="permission_pinning_not_allowed">Fixar não permitido</string>
<!-- Members List -->
<string name="title_members_list">Lista de Membros</string>
<!-- Pinned Messages -->
<string name="title_pinned_messages">Mensagens Pinadas</string>
<!-- Upload Messages -->
<string name="max_file_size_exceeded">Tamanho de arquivo (%1$d bytes) excedeu tamanho máximo de upload (%2$d bytes)</string>
<!-- Socket status -->
<string name="status_connected">conectado</string>
<string name="status_disconnected">desconetado</string>
<string name="status_connecting">conectando</string>
<string name="status_authenticating">autenticando</string>
<string name="status_disconnecting">desconectando</string>
<string name="status_waiting">conectando em %d segundos</string>
<!-- Slash Commands -->
<string name="Slash_Gimme_Description">Exibir ༼ つ ◕_◕ ༽つ antes de sua mensagem</string>
<string name="Slash_LennyFace_Description">Exibir ( ͡° ͜ʖ ͡°) depois de sua mensagem</string>
<string name="Slash_Shrug_Description">Exibir ¯\_(ツ)_/¯ depois de sua mensagem</string>
<string name="Slash_Tableflip_Description">Exibir (╯°□°)╯︵ ┻━┻</string>
<string name="Slash_TableUnflip_Description">Exibir ┬─┬ ノ( ゜-゜ノ)</string>
<string name="Create_A_New_Channel">Criar um novo canal</string>
<string name="Show_the_keyboard_shortcut_list">Show the keyboard shortcut list</string>
<string name="Invite_user_to_join_channel_all_from"> do [#canal] para entrar neste</string>
<string name="Invite_user_to_join_channel_all_to">Convidar todos os usuários deste canal para entrar no [#canal]</string>
<string name="Archive">Arquivar</string>
<string name="Remove_someone_from_room">Remover alguém do canal</string>
<string name="Leave_the_current_channel">Sair do canal atual</string>
<string name="Displays_action_text">Exibir texto de ação</string>
<string name="Direct_message_someone">Enviar DM para alguém</string>
<string name="Mute_someone_in_room">Mutar alguém</string>
<string name="Unmute_someone_in_room">Desmutar alguém na sala</string>
<string name="Invite_user_to_join_channel">Convidar algum usuário para entrar neste canal</string>
<string name="Unarchive">Desarquivar</string>
<string name="Join_the_given_channel">Entrar no canal especificado</string>
<string name="Guggy_Command_Description">Gera um gif baseado no texto dado</string>
<string name="Slash_Topic_Description">Definir tópico</string>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#15293F</color>
<!-- Main colors -->
<color name="colorPrimary">#FF303030</color> <!-- Material Grey 850 -->
<color name="colorPrimaryDark">#ff212121</color> <!-- Material Grey 900 -->
<color name="colorAccent">#FF1976D2</color> <!-- Material Blue 700 -->
<color name="ic_launcher_background">#15293F</color>
<!-- Text colors -->
<color name="colorPrimaryText">#DE000000</color>
<color name="colorSecondaryText">#787878</color>
<!-- User status colors -->
<color name="colorUserStatusOnline">#2FE1A8</color>
<color name="colorUserStatusBusy">#F33E5B</color>
<color name="colorUserStatusAway">#FDD236</color>
<color name="colorUserStatusOffline">#1F2228</color>
<color name="colorUserStatusOffline">#d9d9d9</color>
<color name="colorDrawableTintGrey">#9FA2A8</color>
......@@ -19,16 +25,18 @@
<color name="colorDim">#99000000</color>
<color name="colorBackgroundFileAttachment">#E2FFC8</color>
<color name="colorBackgroundContentFileAttachment">#FFD8F5C0</color>
<color name="colorBackgroundMemberContainer">#4D000000</color>
<color name="white">#FFFFFFFF</color>
<color name="black">#FF000000</color>
<color name="red">#FFFF0000</color>
<color name="darkGray">#a0a0a0</color>
<color name="actionMenuColor">#727272</color>
<color name="darkGray">#FFa0a0a0</color>
<color name="actionMenuColor">#FF727272</color>
<color name="whitesmoke">#FFf1f1f1</color>
<color name="colorEmojiIcon">#FF767676</color>
<color name="linkTextColor">#FF074481</color>
<color name="linkBackgroundColor">#30074481</color>
<!-- Suggestions -->
<color name="suggestion_background_color">@android:color/white</color>
</resources>
......@@ -4,11 +4,36 @@
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="screen_edge_left_and_right_margins">16dp</dimen>
<dimen name="screen_edge_left_and_right_padding">16dp</dimen>
<dimen name="chat_item_top_and_bottom_padding">12dp</dimen>
<dimen name="message_item_top_and_bottom_padding">6dp</dimen>
<dimen name="member_item_top_and_bottom_padding">6dp</dimen>
<dimen name="edit_text_margin">10dp</dimen>
<dimen name="edit_text_drawable_padding">16dp</dimen>
<dimen name="text_view_drawable_padding">4dp</dimen>
<dimen name="fab_elevation">6dp</dimen>
<dimen name="divider_item_decorator_bound_start">72dp</dimen>
<dimen name="divider_item_decorator_bound_end">0dp</dimen>
<dimen name="message_time_text_size">12sp</dimen>
<!-- Emoji -->
<dimen name="picker_padding_bottom">16dp</dimen>
<dimen name="supposed_keyboard_height">252dp</dimen>
<dimen name="picker_popup_height">250dp</dimen>
<dimen name="picker_popup_width">300dp</dimen>
<!-- Message -->
<dimen name="padding_quote">8dp</dimen>
<dimen name="padding_mention">4dp</dimen>
<dimen name="radius_mention">6dp</dimen>
<!-- Autocomplete Popup -->
<dimen name="popup_max_height">150dp</dimen>
<dimen name="suggestions_box_max_height">250dp</dimen>
</resources>
\ No newline at end of file
......@@ -5,19 +5,32 @@
<string name="title_sign_in_your_server">Sign in your server</string>
<string name="title_log_in">Log in</string>
<string name="title_sign_up">Sign up</string>
<string name="title_authentication">Authentication</string>
<string name="title_legal_terms">Legal Terms</string>
<string name="title_chats">Chats</string>
<string name="title_profile">Profile</string>
<string name="title_members">Members (%d)</string>
<string name="title_settings">Settings</string>
<string name="title_password">Change Password</string>
<string name="title_update_profile">Update profile</string>
<!-- Actions -->
<string name="action_connect">Connect</string>
<string name="action_login_or_sign_up">Tap this button to log in or create an account</string>
<string name="action_terms_of_service">Terms of Service</string>
<string name="action_privacy_policy">Privacy Policy</string>
<string name="action_search">Search</string>
<string name="action_update">Update</string>
<string name="action_logout">Log Out</string>
<string name="action_settings">Settings</string>
<string name="action_logout">Logout</string>
<string name="action_files">Files</string>
<string name="action_confirm_password">Confirm Password Change</string>
<string name="action_join_chat">Join Chat</string>
<!-- Settings List -->
<string-array name="settings_actions">
<item name="item_password">Change Password</item>
</string-array>
<!-- Regular information messages -->
<string name="msg_no_internet_connection">No internet connection</string>
......@@ -29,6 +42,7 @@
<string name="msg_password">password</string>
<string name="msg_name">name</string>
<string name="msg_email">email</string>
<string name="msg_avatar_url">avatar URL</string>
<string name="msg_or_continue_using_social_accounts">Or continue using social accounts</string>
<string name="msg_new_user">New user? %1$s</string>
<string name="msg_new_user_agreement">By proceeding you are agreeing to our\n%1$s and %2$s</string>
......@@ -51,6 +65,11 @@
<string name="msg_content_description_show_attachment_options">Show attachment options</string>
<string name="msg_you">You</string>
<string name="msg_unknown">Unknown</string>
<string name="msg_email_address">E-mail address</string>
<string name="msg_utc_offset">UTC offset</string>
<string name="msg_new_password">Enter New Password</string>
<string name="msg_confirm_password">Confirm New Password</string>
<string name="msg_unread_messages">Unread messages</string>
<!-- System messages -->
<string name="message_room_name_changed">Room name changed to: %1$s by %2$s</string>
......@@ -73,13 +92,51 @@
<string name="action_msg_star">Star Message</string>
<string name="action_msg_share">Share</string>
<string name="action_title_editing">Editing Message</string>
<string name="action_msg_add_reaction">Add reaction</string>
<!-- Permission messages -->
<string name="permission_editing_not_allowed">Editing is not allowed</string>
<string name="permission_deleting_not_allowed">Deleting is not allowed</string>
<string name="permission_pinning_not_allowed">Pinning is not allowed</string>
<!-- Members List -->
<string name="title_members_list">Members List</string>
<!-- Pinned Messages -->
<string name="title_pinned_messages">Pinned Messages</string>
<!-- Upload Messages -->
<string name="max_file_size_exceeded">File size %1$d bytes exceeded max upload size of %2$d bytes</string>
<!-- Socket status -->
<string name="status_connected">connected</string>
<string name="status_disconnected">disconnected</string>
<string name="status_connecting">connecting</string>
<string name="status_authenticating">authenticating</string>
<string name="status_disconnecting">disconnecting</string>
<string name="status_waiting">connecting in %d seconds</string>
<!-- Slash Commands -->
<string name="Slash_Gimme_Description">Displays ༼ つ ◕_◕ ༽つ before your message</string>
<string name="Slash_LennyFace_Description">Displays ( ͡° ͜ʖ ͡°) after your message</string>
<string name="Slash_Shrug_Description">Displays ¯\_(ツ)_/¯ after your message</string>
<string name="Slash_Tableflip_Description">Displays (╯°□°)╯︵ ┻━┻</string>
<string name="Slash_TableUnflip_Description">Displays ┬─┬ ノ( ゜-゜ノ)</string>
<string name="Create_A_New_Channel">Create a new channel</string>
<string name="Show_the_keyboard_shortcut_list">Show the keyboard shortcut list</string>
<string name="Invite_user_to_join_channel_all_from">Invite all users from [#channel] to join this channel</string>
<string name="Invite_user_to_join_channel_all_to">Invite all users from this channel to join [#channel]</string>
<string name="Archive">Archive</string>
<string name="Remove_someone_from_room">Remove someone from the room</string>
<string name="Leave_the_current_channel">Leave the current channel</string>
<string name="Displays_action_text">Displays action text</string>
<string name="Direct_message_someone">Direct message someone</string>
<string name="Mute_someone_in_room">Mute someone in the room</string>
<string name="Unmute_someone_in_room">Unmute someone in the room</string>
<string name="Invite_user_to_join_channel">Invite one user to join this channel</string>
<string name="Unarchive">Unarchive</string>
<string name="Join_the_given_channel">Join the given channel</string>
<string name="Guggy_Command_Description">Generates a gif based upon the provided text</string>
<string name="Slash_Topic_Description">Set topic</string>
</resources>
\ No newline at end of file
......@@ -17,7 +17,18 @@
</style>
<!-- Widget styles. -->
<style name="HeadlineTextView" parent="TextAppearance.AppCompat.Headline">
<style name="Authentication.TextView" parent="TextAppearance.AppCompat.Medium">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">50dp</item>
<item name="android:layout_marginStart">@dimen/screen_edge_left_and_right_margins</item>
<item name="android:paddingStart">@dimen/edit_text_margin</item>
<item name="android:maxLines">1</item>
<item name="android:drawablePadding">@dimen/edit_text_drawable_padding</item>
<item name="android:fontFamily">sans-serif</item>
<item name="android:background">@drawable/style_edit_text_authentication</item>
</style>
<style name="Authentication.Headline.TextView" parent="TextAppearance.AppCompat.Headline">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginTop">20dp</item>
......@@ -25,7 +36,7 @@
<item name="android:layout_marginEnd">@dimen/screen_edge_left_and_right_margins</item>
</style>
<style name="EditText.Authentication" parent="Widget.AppCompat.EditText">
<style name="Authentication.EditText" parent="Widget.AppCompat.EditText">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">50dp</item>
<item name="android:layout_marginStart">@dimen/screen_edge_left_and_right_margins</item>
......@@ -39,8 +50,18 @@
<item name="android:background">@drawable/style_edit_text_authentication</item>
</style>
<style name="EditText.Profile" parent="EditText.Authentication">
<item name="android:background">@drawable/style_edit_text_profile</item>
<style name="Authentication.Button" parent="Widget.AppCompat.Button">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:background">@drawable/effect_ripple</item>
</style>
<style name="EditText.Password" parent="TextAppearance.AppCompat">
<!-- Hint color and label color in FALSE state -->
<item name="android:textColorHint">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorPrimaryDark</item>
<item name="colorControlNormal">@color/colorPrimaryDark</item>
<item name="colorControlActivated">@color/colorPrimaryDark</item>
</style>
<style name="AuthenticationLabel" parent="TextAppearance.AppCompat.Medium">
......@@ -48,16 +69,36 @@
<item name="android:layout_height">50dp</item>
<item name="android:layout_marginStart">@dimen/screen_edge_left_and_right_margins</item>
<item name="android:paddingStart">@dimen/edit_text_margin</item>
</style>
<style name="ChatRoom.Name.TextView" parent="TextAppearance.AppCompat.Title">
<item name="android:ellipsize">end</item>
<item name="android:maxLines">1</item>
<item name="android:drawablePadding">@dimen/edit_text_drawable_padding</item>
<item name="android:fontFamily">sans-serif</item>
<item name="android:background">@drawable/style_edit_text_authentication</item>
<item name="android:textSize">16sp</item>
</style>
<style name="AuthenticationButton" parent="Widget.AppCompat.Button">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:background">@drawable/effect_ripple</item>
<style name="Sender.Name.TextView" parent="TextAppearance.AppCompat.Title">
<item name="android:ellipsize">end</item>
<item name="android:maxLines">1</item>
<item name="android:textSize">16sp</item>
</style>
<style name="Message.TextView" parent="android:TextAppearance">
<item name="android:textSize">15sp</item>
<item name="android:textColor">@color/colorPrimaryText</item>
</style>
<style name="Timestamp.TextView" parent="TextAppearance.AppCompat.Caption">
<item name="android:textSize">10sp</item>
</style>
<style name="Profile.EditText" parent="Authentication.EditText">
<item name="android:background">@drawable/style_edit_text_profile</item>
</style>
<style name="ActionModeStyle" parent="ThemeOverlay.AppCompat.Dark.ActionBar">
<item name="android:actionModeCloseDrawable">@drawable/ic_close_white_24dp</item>
<item name="actionModeCloseDrawable">@drawable/ic_close_white_24dp</item>
</style>
</resources>
\ No newline at end of file
......@@ -13,8 +13,8 @@ buildscript {
classpath "com.android.tools.build:gradle:3.0.1"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}"
classpath 'com.google.gms:google-services:3.1.2'
classpath 'io.fabric.tools:gradle:1.+'
classpath 'com.google.gms:google-services:3.2.0'
classpath 'io.fabric.tools:gradle:1.25.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
......@@ -27,7 +27,6 @@ allprojects {
jcenter()
maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
maven { url "https://jitpack.io" }
maven { url "http://dl.bintray.com/amulyakhare/maven" } // For TextDrawable.
}
apply from: rootProject.file('dependencies.gradle')
......
#
# Build configuration for Circle CI
#
# See this thread for speeding up and caching directories: https://discuss.circleci.com/t/installing-android-build-tools-23-0-2/924
#
machine:
environment:
ANDROID_HOME: /usr/local/android-sdk-linux
GRADLE_OPTS: '-Xmx1536M -Dorg.gradle.jvmargs="-Xmx1536M -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError"'
JAVA_OPTS: "-Xms518m -Xmx1536M"
pre:
- git clone https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK.git Rocket.Chat.Kotlin.SDK
dependencies:
pre:
- sudo service mysql stop; sleep 5
- sudo service mongod stop; sleep 5
- sudo killall postgres; sleep 5
- git fetch --tags
- echo "sdk.dir="$ANDROID_HOME > local.properties
- echo $ROCKET_JKS_BASE64 | base64 --decode > Rocket.jks
- echo $ROCKET_PLAY_JSON | base64 --decode > app/rocket-chat.json
- mkdir -p app/src/release/res/values
# TODO: remove the comment when using that file on the project
# - echo $GOOGLE_SERVICES_BASE64 | base64 --decode > app/src/release/google-services.json
# - echo $API_KEY_STRINGS_BASE64 | base64 --decode > app/src/release/res/values/api_key_strings.xml
- mkdir -p $ANDROID_HOME/licenses/
- echo 8933bad161af4178b1185d1a37fbf41ea5269c55 >> $ANDROID_HOME/licenses/android-sdk-license
- echo d56f5187479451eabf01fb78af6dfcb131a6481e >> $ANDROID_HOME/licenses/android-sdk-license
- echo y | android update sdk --no-ui --all --filter tools,platform-tools
- echo y | android update sdk --no-ui --all --filter android-27
- echo y | android update sdk --no-ui --all --filter extra-android-m2repository,extra-android-support
- echo y | android update sdk --no-ui --all --filter extra-google-m2repository,extra-google-google_play_services
- echo y | android update sdk --no-ui --all --filter build-tools-27.0.0
#- yes | sdkmanager --licenses
cache_directories:
#- /usr/local/android-sdk-linux/tools
#- /usr/local/android-sdk-linux/build-tools/27.0.0
test:
override:
- ./gradlew assembleRelease --stacktrace
- find . -name *.apk -exec mv {} $CIRCLE_ARTIFACTS/ \;
deployment:
beta:
tag: /v\d+\.\d+\.\d+(?!.)/
owner: RocketChat
commands:
- ./gradlew publishListingRelease
-Dorg.gradle.project.track=beta
alpha:
tag: /v\d+\.\d+\.\d+/
owner: RocketChat
commands:
- ./gradlew publishListingRelease
-Dorg.gradle.pr oject.track=alpha
......@@ -4,33 +4,32 @@ ext {
compileSdk : 27,
targetSdk : 27,
buildTools : '27.0.3',
kotlin : '1.2.21',
kotlin : '1.2.30',
coroutine : '0.22',
dokka : '0.9.15',
// Main dependencies
support : '27.0.2',
constraintLayout : '1.0.2',
dagger : '2.13',
androidKtx : '0.1',
dagger : '2.14.1',
exoPlayer : '2.6.0',
playServices : '11.8.0',
room : '1.0.0',
rxKotlin : '2.2.0',
rxAndroid : '2.0.1',
rxAndroid : '2.0.2',
moshi : '1.6.0-SNAPSHOT',
okhttp : '3.9.0',
timber : '4.5.1',
okhttp : '3.9.1',
timber : '4.6.1',
threeTenABP : '1.0.5',
rxBinding : '2.0.0',
fresco : '1.7.1',
fresco : '1.8.1',
kotshi : '0.3.0',
frescoImageViewer : '0.5.0',
androidSvg : '1.2.1',
aVLoadingIndicatorView : '2.1.3',
frescoImageViewer : '0.5.1',
markwon : '1.0.3',
textDrawable : '1.0.2', // Remove this library after https://github.com/RocketChat/Rocket.Chat/pull/9492 is merged
moshiLazyAdapters : '2.1', // Even declared on the SDK we need to add this library here because java.lang.NoClassDefFoundError: Failed resolution of: Lcom/serjltt/moshi/adapters/FallbackEnum;
sheetMenu : '1.3.3',
aVLoadingIndicatorView : '2.1.3',
flexbox : '0.3.2',
// For testing
junit : '4.12',
......@@ -50,6 +49,9 @@ ext {
design : "com.android.support:design:${versions.support}",
constraintLayout : "com.android.support.constraint:constraint-layout:${versions.constraintLayout}",
cardView : "com.android.support:cardview-v7:${versions.support}",
flexbox : "com.google.android:flexbox:${versions.flexbox}",
androidKtx : "androidx.core:core-ktx:${versions.androidKtx}",
dagger : "com.google.dagger:dagger:${versions.dagger}",
daggerSupport : "com.google.dagger:dagger-android-support:${versions.dagger}",
......@@ -83,21 +85,15 @@ ext {
kotshiApi : "se.ansman.kotshi:api:${versions.kotshi}",
kotshiCompiler : "se.ansman.kotshi:compiler:${versions.kotshi}",
frescoImageViewer : "com.github.stfalcon:frescoimageviewer:${versions.frescoImageViewer}",
androidSvg : "com.caverock:androidsvg:${versions.androidSvg}",
aVLoadingIndicatorView : "com.wang.avi:library:${versions.aVLoadingIndicatorView}",
textDrawable : "com.github.rocketchat:textdrawable:${versions.textDrawable}",
frescoImageViewer : "com.github.luciofm:FrescoImageViewer:${versions.frescoImageViewer}",
markwon : "ru.noties:markwon:${versions.markwon}",
markwonImageLoader : "ru.noties:markwon-image-loader:${versions.markwon}",
moshiLazyAdapters : "com.serjltt.moshi:moshi-lazy-adapters:${versions.moshiLazyAdapters}",
sheetMenu : "com.github.whalemare:sheetmenu:${versions.sheetMenu}",
aVLoadingIndicatorView : "com.wang.avi:library:${versions.aVLoadingIndicatorView}",
// For testing
junit : "junit:junit:$versions.junit",
expressoCore : "com.android.support.test.espresso:espresso-core:${versions.expresso}",
......
/**
* Generator steps:
*
* 1. Download EmojiOne json file from: https://raw.githubusercontent.com/emojione/emojione/master/emoji.json
* 2. Install sdkman, kotlin and kscript for cli usage
* 3. Run: kscript generator.kts
*
* This file will output a json file named emoji-parsed.json
*/
@file:DependsOn("org.json:json:20090211")
import org.json.JSONArray
import org.json.JSONObject
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
import java.util.*
val stream = File("emoji.json").inputStream()
val sb = StringBuilder()
val isr = InputStreamReader(stream, Charsets.UTF_8)
val br = BufferedReader(isr)
var read: String? = br.readLine()
while (read != null) {
sb.append(read)
read = br.readLine()
}
br.close()
val json = JSONObject(sb.toString())
val all = JSONArray()
val jsonList = mutableListOf<JSONObject>()
json.keys().forEach {
val oldJson = json.getJSONObject(it as String) as JSONObject
val newJson = JSONObject().apply {
put("shortname", oldJson.getString("shortname"))
put("category", oldJson.getString("category"))
put("shortnameAlternates", oldJson.getJSONArray("shortname_alternates"))
put("keywords", oldJson.getJSONArray("keywords"))
put("order", oldJson.getInt("order"))
val codePoints = oldJson.get("code_points") as JSONObject
val unicode = codePoints.getString("fully_qualified")
put("unicode", unicode)
}
all.put(newJson)
jsonList.add(newJson)
}
Collections.sort(jsonList, { o1, o2 ->
val order1 = o1.getInt("order")
val order2 = o2.getInt("order")
return@sort order1 - order2
})
File("emoji-parsed.json").printWriter(Charsets.UTF_8).use { out ->
out.println("[")
for (i in 0..jsonList.size - 1) {
out.print(jsonList.get(i).toString(2))
if (i < jsonList.size - 1) {
out.println(",")
}
}
out.println("]")
}
println("Ok")
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