Commit b90b3ab9 authored by samer's avatar samer

Merge branch 'develop-2.x' of https://github.com/RocketChat/Rocket.Chat.Android into develop-2.x

parents e87cc066 8bab94e9
...@@ -12,8 +12,8 @@ android { ...@@ -12,8 +12,8 @@ android {
applicationId "chat.rocket.android" applicationId "chat.rocket.android"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion versions.targetSdk targetSdkVersion versions.targetSdk
versionCode 1008 versionCode 1009
versionName "2.0.0-dev6" versionName "2.0.0-dev7"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
} }
...@@ -105,8 +105,6 @@ dependencies { ...@@ -105,8 +105,6 @@ dependencies {
implementation libraries.aVLoadingIndicatorView implementation libraries.aVLoadingIndicatorView
implementation libraries.swipeBackLayout
implementation('com.crashlytics.sdk.android:crashlytics:2.6.8@aar') { implementation('com.crashlytics.sdk.android:crashlytics:2.6.8@aar') {
transitive = true transitive = true
} }
......
#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,22 @@ ...@@ -37,18 +37,22 @@
<activity <activity
android:name=".main.ui.MainActivity" android:name=".main.ui.MainActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" /> android:theme="@style/AppTheme" />
<activity <activity
android:name=".webview.WebViewActivity" android:name=".webview.WebViewActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" /> android:theme="@style/AppTheme" />
<activity <activity
android:name=".chatroom.ui.ChatRoomActivity" android:name=".chatroom.ui.ChatRoomActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" /> android:theme="@style/AppTheme" />
<activity <activity
android:name=".chatroom.ui.PinnedMessagesActivity" android:name=".chatroom.ui.PinnedMessagesActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" /> android:theme="@style/AppTheme" />
<activity <activity
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -9,6 +9,7 @@ import chat.rocket.android.helper.CrashlyticsTree ...@@ -9,6 +9,7 @@ import chat.rocket.android.helper.CrashlyticsTree
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.MultiServerTokenRepository import chat.rocket.android.server.domain.MultiServerTokenRepository
import chat.rocket.android.server.domain.SettingsRepository import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.widget.emoji.EmojiRepository
import chat.rocket.common.model.Token import chat.rocket.common.model.Token
import chat.rocket.core.TokenRepository import chat.rocket.core.TokenRepository
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
...@@ -57,6 +58,7 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje ...@@ -57,6 +58,7 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
initCurrentServer() initCurrentServer()
AndroidThreeTen.init(this) AndroidThreeTen.init(this)
EmojiRepository.load(this)
setupCrashlytics() setupCrashlytics()
setupFresco() setupFresco()
......
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 kotlinx.android.synthetic.main.message_attachment.view.*
class AudioAttachmentViewHolder(itemView: View) : BaseViewHolder<AudioAttachmentViewModel>(itemView) {
init {
with(itemView) {
image_attachment.setVisible(false)
audio_video_attachment.setVisible(true)
}
}
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.v7.widget.RecyclerView
import android.view.View
abstract class BaseViewHolder<T>(itemView: View) : RecyclerView.ViewHolder(itemView) {
var data: T? = null
fun bind(data: T) {
this.data = data
bindViews(data)
}
abstract fun bindViews(data: T)
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.support.v7.widget.RecyclerView
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 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
) : 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, roomName, roomType, presenter, enableActions)
}
BaseViewModel.ViewType.IMAGE_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment)
ImageAttachmentViewHolder(view)
}
BaseViewModel.ViewType.AUDIO_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment)
AudioAttachmentViewHolder(view)
}
BaseViewModel.ViewType.VIDEO_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment)
VideoAttachmentViewHolder(view)
}
BaseViewModel.ViewType.URL_PREVIEW -> {
val view = parent.inflate(R.layout.message_url_preview)
UrlPreviewViewHolder(view)
}
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) {
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<*>>) {
this.dataSet.addAll(0, dataSet)
notifyItemRangeInserted(0, dataSet.size)
}
fun updateItem(message: BaseViewModel<*>) {
val index = dataSet.indexOfLast { it.messageId == message.messageId }
Timber.d("index: $index")
if (index > -1) {
dataSet[index] = message
notifyItemChanged(index)
}
}
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)
}
}
}
\ 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.stfalcon.frescoimageviewer.ImageViewer
import kotlinx.android.synthetic.main.message_attachment.view.*
class ImageAttachmentViewHolder(itemView: View) : BaseViewHolder<ImageAttachmentViewModel>(itemView) {
override fun bindViews(data: ImageAttachmentViewModel) {
with(itemView) {
image_attachment.setImageURI(data.attachmentUrl)
file_name.text = data.attachmentTitle
image_attachment.setOnClickListener { view ->
// TODO - implement a proper image viewer with a proper Transition
ImageViewer.Builder(view.context, listOf(data.attachmentUrl))
.setStartPosition(0)
.show()
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.text.method.LinkMovementMethod
import android.view.MenuItem
import android.view.View
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.MessageViewModel
import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_message.view.*
import ru.whalemare.sheetmenu.extension.inflate
import ru.whalemare.sheetmenu.extension.toList
class MessageViewHolder(
itemView: View,
private val roomType: String,
private val roomName: String,
private val presenter: ChatRoomPresenter?,
enableActions: Boolean
) : BaseViewHolder<MessageViewModel>(itemView),
MenuItem.OnMenuItemClickListener {
init {
itemView.text_content.movementMethod = LinkMovementMethod()
if (enableActions) {
itemView.setOnLongClickListener {
if (data?.isSystemMessage == false) {
val menuItems = it.context.inflate(R.menu.message_actions).toList()
menuItems.find { it.itemId == R.id.action_menu_msg_pin_unpin }?.apply {
val isPinned = data?.isPinned ?: false
setTitle(if (isPinned) R.string.action_msg_unpin else R.string.action_msg_pin)
isChecked = isPinned
}
val adapter = ActionListAdapter(menuItems, this@MessageViewHolder)
BottomSheetMenu(adapter).apply {
}.show(it.context)
}
true
}
}
}
override fun bindViews(data: MessageViewModel) {
with(itemView) {
text_message_time.text = data.time
text_sender.text = data.senderName
text_content.text = data.content
image_avatar.setImageURI(data.avatar)
}
}
override fun onMenuItemClick(item: MenuItem): Boolean {
data?.rawData?.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)
R.id.action_menu_msg_pin_unpin -> {
with(item) {
if (!isChecked) {
presenter?.pinMessage(id)
} else {
presenter?.unpinMessage(id)
}
}
}
else -> TODO("Not implemented")
}
}
return true
}
}
\ 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 kotlinx.android.synthetic.main.message_url_preview.view.*
class UrlPreviewViewHolder(itemView: View) : BaseViewHolder<UrlPreviewViewModel>(itemView) {
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 kotlinx.android.synthetic.main.message_attachment.view.*
class VideoAttachmentViewHolder(itemView: View) : BaseViewHolder<VideoAttachmentViewModel>(itemView) {
init {
with(itemView) {
image_attachment.setVisible(false)
audio_video_attachment.setVisible(true)
}
}
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
...@@ -3,7 +3,7 @@ package chat.rocket.android.chatroom.presentation ...@@ -3,7 +3,7 @@ package chat.rocket.android.chatroom.presentation
import android.net.Uri import android.net.Uri
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.domain.UriInteractor 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.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
...@@ -34,7 +34,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -34,7 +34,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
private val uriInteractor: UriInteractor, private val uriInteractor: UriInteractor,
private val messagesRepository: MessagesRepository, private val messagesRepository: MessagesRepository,
factory: RocketChatClientFactory, factory: RocketChatClientFactory,
private val mapper: MessageViewModelMapper) { private val mapper: ViewModelMapper) {
private val client = factory.create(serverInteractor.get()!!) private val client = factory.create(serverInteractor.get()!!)
private var subId: String? = null private var subId: String? = null
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!! private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!!
...@@ -48,7 +48,11 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -48,7 +48,11 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
client.messages(chatRoomId, roomTypeOf(chatRoomType), offset, 30).result client.messages(chatRoomId, roomTypeOf(chatRoomType), offset, 30).result
messagesRepository.saveAll(messages) messagesRepository.saveAll(messages)
val messagesViewModels = mapper.mapToViewModelList(messages, settings) // 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)
val messagesViewModels = mapper.map(messages)
view.showMessages(messagesViewModels) view.showMessages(messagesViewModels)
// Subscribe after getting the first page of messages from REST // Subscribe after getting the first page of messages from REST
...@@ -127,6 +131,17 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -127,6 +131,17 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
} }
} }
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.
}
}
}
private fun subscribeMessages(roomId: String) { private fun subscribeMessages(roomId: String) {
client.addStateChannel(stateChannel) client.addStateChannel(stateChannel)
launch(CommonPool + strategy.jobs) { launch(CommonPool + strategy.jobs) {
...@@ -232,9 +247,9 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -232,9 +247,9 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
is RoomType.Custom -> "custom" //TODO: put appropriate callback string here. is RoomType.Custom -> "custom" //TODO: put appropriate callback string here.
} }
view.showReplyingAction( view.showReplyingAction(
user, username = user,
"[ ](${serverUrl}/${room}/${roomName}?msg=${id}) ${mention} ", replyMarkdown = "[ ]($serverUrl/$room/$roomName?msg=$id) $mention ",
m.message quotedMessage = m.message
) )
} }
} }
...@@ -315,7 +330,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -315,7 +330,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
private fun updateMessage(streamedMessage: Message) { private fun updateMessage(streamedMessage: Message) {
launchUI(strategy) { launchUI(strategy) {
val viewModelStreamedMessage = mapper.mapToViewModel(streamedMessage, settings) val viewModelStreamedMessage = mapper.map(streamedMessage)
val roomMessages = messagesRepository.getByRoomId(streamedMessage.roomId) val roomMessages = messagesRepository.getByRoomId(streamedMessage.roomId)
val index = roomMessages.indexOfFirst { msg -> msg.id == streamedMessage.id } val index = roomMessages.indexOfFirst { msg -> msg.id == streamedMessage.id }
if (index > -1) { if (index > -1) {
......
package chat.rocket.android.chatroom.presentation package chat.rocket.android.chatroom.presentation
import android.net.Uri import android.net.Uri
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.LoadingView
import chat.rocket.android.core.behaviours.MessageView import chat.rocket.android.core.behaviours.MessageView
...@@ -12,7 +12,7 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -12,7 +12,7 @@ interface ChatRoomView : LoadingView, MessageView {
* *
* @param dataSet The data set to show. * @param dataSet The data set to show.
*/ */
fun showMessages(dataSet: List<MessageViewModel>) fun showMessages(dataSet: List<BaseViewModel<*>>)
/** /**
* Send a message to a chat room. * Send a message to a chat room.
...@@ -43,7 +43,7 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -43,7 +43,7 @@ interface ChatRoomView : LoadingView, MessageView {
* *
* @param message The (recent) message sent to a chat room. * @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. * Dispatch to the recycler views adapter that we should remove a message.
...@@ -57,7 +57,7 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -57,7 +57,7 @@ interface ChatRoomView : LoadingView, MessageView {
* *
* @param index The index of the changed message * @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. * Show reply status above the message composer.
......
package chat.rocket.android.chatroom.presentation package chat.rocket.android.chatroom.presentation
import chat.rocket.android.chatroom.viewmodel.MessageViewModelMapper import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.chatroom.viewmodel.ViewModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetChatRoomsInteractor import chat.rocket.android.server.domain.GetChatRoomsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
...@@ -18,7 +19,7 @@ class PinnedMessagesPresenter @Inject constructor(private val view: PinnedMessag ...@@ -18,7 +19,7 @@ class PinnedMessagesPresenter @Inject constructor(private val view: PinnedMessag
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val serverInteractor: GetCurrentServerInteractor, private val serverInteractor: GetCurrentServerInteractor,
private val roomsInteractor: GetChatRoomsInteractor, private val roomsInteractor: GetChatRoomsInteractor,
private val mapper: MessageViewModelMapper, private val mapper: ViewModelMapper,
factory: RocketChatClientFactory, factory: RocketChatClientFactory,
getSettingsInteractor: GetSettingsInteractor) { getSettingsInteractor: GetSettingsInteractor) {
...@@ -41,8 +42,8 @@ class PinnedMessagesPresenter @Inject constructor(private val view: PinnedMessag ...@@ -41,8 +42,8 @@ class PinnedMessagesPresenter @Inject constructor(private val view: PinnedMessag
val pinnedMessages = val pinnedMessages =
client.getRoomPinnedMessages(roomId, room.type, pinnedMessagesListOffset) client.getRoomPinnedMessages(roomId, room.type, pinnedMessagesListOffset)
pinnedMessagesListOffset = pinnedMessages.offset.toInt() pinnedMessagesListOffset = pinnedMessages.offset.toInt()
val messageList = mapper.mapToViewModelList(pinnedMessages.result, settings) val messageList = mapper.map(pinnedMessages.result)
.filterNot { it.isSystemMessage } .filter { it is MessageViewModel}.filterNot { (it as MessageViewModel).isSystemMessage }
view.showPinnedMessages(messageList) view.showPinnedMessages(messageList)
view.hideLoading() view.hideLoading()
}.ifNull { }.ifNull {
......
package chat.rocket.android.chatroom.presentation 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.LoadingView
import chat.rocket.android.core.behaviours.MessageView import chat.rocket.android.core.behaviours.MessageView
...@@ -11,5 +11,5 @@ interface PinnedMessagesView : MessageView, LoadingView { ...@@ -11,5 +11,5 @@ interface PinnedMessagesView : MessageView, LoadingView {
* *
* @param pinnedMessages The list of pinned messages. * @param pinnedMessages The list of pinned messages.
*/ */
fun showPinnedMessages(pinnedMessages: List<MessageViewModel>) fun showPinnedMessages(pinnedMessages: List<BaseViewModel<*>>)
} }
\ No newline at end of file
...@@ -13,8 +13,6 @@ import dagger.android.AndroidInjector ...@@ -13,8 +13,6 @@ import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.app_bar_chat_room.* import kotlinx.android.synthetic.main.app_bar_chat_room.*
import me.imid.swipebacklayout.lib.SwipeBackLayout
import me.imid.swipebacklayout.lib.app.SwipeBackActivity
import javax.inject.Inject import javax.inject.Inject
...@@ -32,7 +30,7 @@ private const val INTENT_CHAT_ROOM_NAME = "chat_room_name" ...@@ -32,7 +30,7 @@ private const val INTENT_CHAT_ROOM_NAME = "chat_room_name"
private const val INTENT_CHAT_ROOM_TYPE = "chat_room_type" 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_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only"
class ChatRoomActivity : SwipeBackActivity(), HasSupportFragmentInjector { class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment> @Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
private lateinit var chatRoomId: String private lateinit var chatRoomId: String
private lateinit var chatRoomName: String private lateinit var chatRoomName: String
...@@ -56,8 +54,6 @@ class ChatRoomActivity : SwipeBackActivity(), HasSupportFragmentInjector { ...@@ -56,8 +54,6 @@ class ChatRoomActivity : SwipeBackActivity(), HasSupportFragmentInjector {
isChatRoomReadOnly = intent.getBooleanExtra(INTENT_IS_CHAT_ROOM_READ_ONLY, true) isChatRoomReadOnly = intent.getBooleanExtra(INTENT_IS_CHAT_ROOM_READ_ONLY, true)
requireNotNull(chatRoomType) { "no is_chat_room_read_only provided in Intent extras" } requireNotNull(chatRoomType) { "no is_chat_room_read_only provided in Intent extras" }
swipeBackLayout.setEdgeTrackingEnabled(SwipeBackLayout.EDGE_LEFT)
setupToolbar(chatRoomName) setupToolbar(chatRoomName)
addFragment("ChatRoomFragment", R.id.fragment_container) { addFragment("ChatRoomFragment", R.id.fragment_container) {
...@@ -65,7 +61,9 @@ class ChatRoomActivity : SwipeBackActivity(), HasSupportFragmentInjector { ...@@ -65,7 +61,9 @@ class ChatRoomActivity : SwipeBackActivity(), HasSupportFragmentInjector {
} }
} }
override fun onBackPressed() = finishActivity() override fun onBackPressed() {
finishActivity()
}
override fun supportFragmentInjector(): AndroidInjector<Fragment> { override fun supportFragmentInjector(): AndroidInjector<Fragment> {
return fragmentDispatchingAndroidInjector return fragmentDispatchingAndroidInjector
......
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,18 +8,25 @@ import android.content.Intent ...@@ -8,18 +8,25 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.support.annotation.DrawableRes
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.support.v7.widget.DefaultItemAnimator import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.view.* import android.view.*
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.ChatRoomAdapter
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.presentation.ChatRoomView import chat.rocket.android.chatroom.presentation.ChatRoomView
import chat.rocket.android.chatroom.viewmodel.MessageViewModel import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.MessageParser import chat.rocket.android.helper.MessageParser
import chat.rocket.android.util.extensions.* import chat.rocket.android.util.extensions.*
import chat.rocket.android.widget.emoji.ComposerEditText
import chat.rocket.android.widget.emoji.Emoji
import chat.rocket.android.widget.emoji.EmojiKeyboardPopup
import chat.rocket.android.widget.emoji.EmojiParser
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import kotlinx.android.synthetic.main.fragment_chat_room.* import kotlinx.android.synthetic.main.fragment_chat_room.*
...@@ -46,7 +53,7 @@ private const val BUNDLE_CHAT_ROOM_TYPE = "chat_room_type" ...@@ -46,7 +53,7 @@ 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 BUNDLE_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only"
private const val REQUEST_CODE_FOR_PERFORM_SAF = 42 private const val REQUEST_CODE_FOR_PERFORM_SAF = 42
class ChatRoomFragment : Fragment(), ChatRoomView { class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
@Inject lateinit var presenter: ChatRoomPresenter @Inject lateinit var presenter: ChatRoomPresenter
@Inject lateinit var parser: MessageParser @Inject lateinit var parser: MessageParser
private lateinit var adapter: ChatRoomAdapter private lateinit var adapter: ChatRoomAdapter
...@@ -54,6 +61,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView { ...@@ -54,6 +61,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
private lateinit var chatRoomId: String private lateinit var chatRoomId: String
private lateinit var chatRoomName: String private lateinit var chatRoomName: String
private lateinit var chatRoomType: String private lateinit var chatRoomType: String
private lateinit var emojiKeyboardPopup: EmojiKeyboardPopup
private var isChatRoomReadOnly: Boolean = false private var isChatRoomReadOnly: Boolean = false
private lateinit var actionSnackbar: ActionSnackbar private lateinit var actionSnackbar: ActionSnackbar
...@@ -98,6 +106,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView { ...@@ -98,6 +106,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
setupActionSnackbar() setupActionSnackbar()
} }
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
text_message.addTextChangedListener(EmojiKeyboardPopup.EmojiTextWatcher(text_message))
}
override fun onDestroyView() { override fun onDestroyView() {
presenter.unsubscribeMessages() presenter.unsubscribeMessages()
handler.removeCallbacksAndMessages(null) handler.removeCallbacksAndMessages(null)
...@@ -132,12 +145,13 @@ class ChatRoomFragment : Fragment(), ChatRoomView { ...@@ -132,12 +145,13 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
return true return true
} }
override fun showMessages(dataSet: List<MessageViewModel>) { override fun showMessages(dataSet: List<BaseViewModel<*>>) {
activity?.apply { activity?.apply {
if (recycler_view.adapter == null) { if (recycler_view.adapter == null) {
adapter = ChatRoomAdapter(chatRoomType, chatRoomName, presenter) adapter = ChatRoomAdapter(chatRoomType, chatRoomName, presenter)
recycler_view.adapter = adapter recycler_view.adapter = adapter
val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true) val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
linearLayoutManager.stackFromEnd = true
recycler_view.layoutManager = linearLayoutManager recycler_view.layoutManager = linearLayoutManager
recycler_view.itemAnimator = DefaultItemAnimator() recycler_view.itemAnimator = DefaultItemAnimator()
if (dataSet.size >= 30) { if (dataSet.size >= 30) {
...@@ -148,7 +162,12 @@ class ChatRoomFragment : Fragment(), ChatRoomView { ...@@ -148,7 +162,12 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
}) })
} }
} }
adapter.addDataSet(dataSet)
val oldMessagesCount = adapter.itemCount
adapter.appendData(dataSet)
if (oldMessagesCount == 0 && dataSet.isNotEmpty()) {
recycler_view.scrollToPosition(0)
}
} }
} }
...@@ -165,8 +184,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView { ...@@ -165,8 +184,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
override fun showInvalidFileMessage() = showMessage(getString(R.string.msg_invalid_file)) override fun showInvalidFileMessage() = showMessage(getString(R.string.msg_invalid_file))
override fun showNewMessage(message: MessageViewModel) { override fun showNewMessage(message: List<BaseViewModel<*>>) {
adapter.addItem(message) adapter.prependData(message)
recycler_view.scrollToPosition(0) recycler_view.scrollToPosition(0)
} }
...@@ -176,6 +195,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView { ...@@ -176,6 +195,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
override fun enableSendMessageButton() { override fun enableSendMessageButton() {
button_send.isEnabled = true button_send.isEnabled = true
text_message.isEnabled = true
text_message.erase()
} }
override fun clearMessageComposition() { override fun clearMessageComposition() {
...@@ -185,8 +206,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView { ...@@ -185,8 +206,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
actionSnackbar.dismiss() actionSnackbar.dismiss()
} }
override fun dispatchUpdateMessage(index: Int, message: MessageViewModel) { override fun dispatchUpdateMessage(index: Int, message: List<BaseViewModel<*>>) {
adapter.updateItem(message) adapter.updateItem(message.last())
} }
override fun dispatchDeleteMessage(msgId: String) { override fun dispatchDeleteMessage(msgId: String) {
...@@ -229,6 +250,28 @@ class ChatRoomFragment : Fragment(), ChatRoomView { ...@@ -229,6 +250,28 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
} }
} }
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)
}
}
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")
}
}
private fun setReactionButtonIcon(@DrawableRes drawableId: Int) {
button_add_reaction.setImageResource(drawableId)
button_add_reaction.setTag(drawableId)
}
override fun showFileSelection(filter: Array<String>) { override fun showFileSelection(filter: Array<String>) {
val intent = Intent(Intent.ACTION_GET_CONTENT) val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*" intent.type = "*/*"
...@@ -268,11 +311,30 @@ class ChatRoomFragment : Fragment(), ChatRoomView { ...@@ -268,11 +311,30 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
input_container.setVisible(false) input_container.setVisible(false)
} else { } else {
subscribeTextMessage() subscribeTextMessage()
emojiKeyboardPopup = EmojiKeyboardPopup(activity!!, activity!!.findViewById(R.id.fragment_container))
emojiKeyboardPopup.listener = this
text_message.listener = object : ComposerEditText.ComposerEditTextListener {
override fun onKeyboardOpened() {
}
override fun onKeyboardClosed() {
activity?.let {
if (!emojiKeyboardPopup.isKeyboardOpen) {
it.onBackPressed()
}
KeyboardHelper.hideSoftKeyboard(it)
emojiKeyboardPopup.dismiss()
}
setReactionButtonIcon(R.drawable.ic_reaction_24dp)
}
}
button_send.setOnClickListener { button_send.setOnClickListener {
var message = citation ?: "" var textMessage = citation ?: ""
message += text_message.textContent textMessage += text_message.textContent
sendMessage(message) sendMessage(textMessage)
clearMessageComposition()
} }
button_show_attachment_options.setOnClickListener { button_show_attachment_options.setOnClickListener {
...@@ -296,6 +358,31 @@ class ChatRoomFragment : Fragment(), ChatRoomView { ...@@ -296,6 +358,31 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
hideAttachmentOptions() hideAttachmentOptions()
}, 400) }, 400)
} }
button_add_reaction.setOnClickListener { view ->
openEmojiKeyboardPopup()
}
}
}
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.setFocusableInTouchMode(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)
} }
} }
......
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 ...@@ -9,9 +9,10 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.ChatRoomAdapter
import chat.rocket.android.chatroom.presentation.PinnedMessagesPresenter import chat.rocket.android.chatroom.presentation.PinnedMessagesPresenter
import chat.rocket.android.chatroom.presentation.PinnedMessagesView 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.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.util.extensions.setVisible import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast import chat.rocket.android.util.extensions.showToast
...@@ -39,7 +40,7 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView { ...@@ -39,7 +40,7 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
private lateinit var chatRoomId: String private lateinit var chatRoomId: String
private lateinit var chatRoomName: String private lateinit var chatRoomName: String
private lateinit var chatRoomType: String private lateinit var chatRoomType: String
private lateinit var adapter: PinnedMessagesAdapter private lateinit var adapter: ChatRoomAdapter
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
...@@ -71,10 +72,11 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView { ...@@ -71,10 +72,11 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error)) override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showPinnedMessages(pinnedMessages: List<MessageViewModel>) { override fun showPinnedMessages(pinnedMessages: List<BaseViewModel<*>>) {
activity?.apply { activity?.apply {
if (recycler_view_pinned.adapter == null) { 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 recycler_view_pinned.adapter = adapter
val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
recycler_view_pinned.layoutManager = linearLayoutManager recycler_view_pinned.layoutManager = linearLayoutManager
...@@ -88,7 +90,7 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView { ...@@ -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.attachment.AudioAttachment
data class AudioAttachmentViewModel(
override val rawData: AudioAttachment,
override val messageId: String,
override val attachmentUrl: String,
override val attachmentTitle: CharSequence,
override val id: Long
) : 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 java.security.InvalidParameterException
interface BaseViewModel<out T> {
val rawData: T
val messageId: String
val viewType: Int
val layoutId: Int
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.attachment.ImageAttachment
data class ImageAttachmentViewModel(
override val rawData: ImageAttachment,
override val messageId: String,
override val attachmentUrl: String,
override val attachmentTitle: CharSequence,
override val id: Long
) : 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 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.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.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, data class MessageViewModel(
private val token: Token?, override val rawData: Message,
private val message: Message, override val messageId: String,
private val settings: Map<String, Value<Any>>, override val avatar: String,
private val parser: MessageParser, override val time: CharSequence,
private val messagesRepository: MessagesRepository, override val senderName: CharSequence,
private val localRepository: LocalRepository, override val content: CharSequence,
private val currentServerRepository: CurrentServerRepository) { override val isPinned: Boolean,
val id: String = message.id val isSystemMessage: Boolean
val avatarUri: String? ) : BaseMessageViewModel<Message> {
val roomId: String = message.roomId override val viewType: Int
val time: CharSequence get() = BaseViewModel.ViewType.MESSAGE.viewType
val senderName: CharSequence
val content: CharSequence override val layoutId: Int
var quote: Message? = null get() = R.layout.item_message
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()
} }
\ 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
import chat.rocket.android.R
import chat.rocket.core.model.url.Url
data class UrlPreviewViewModel(
override val rawData: Url,
override val messageId: String,
val title: CharSequence?,
val hostname: String,
val description: CharSequence?,
val thumbUrl: String?
) : 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.attachment.VideoAttachment
data class VideoAttachmentViewModel(
override val rawData: VideoAttachment,
override val messageId: String,
override val attachmentUrl: String,
override val attachmentTitle: CharSequence,
override val id: Long
) : 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.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.url.Url
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
import okhttp3.HttpUrl
import timber.log.Timber
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(url, message.id, title, hostname, description, thumb)
}
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("$baseUrl${attachment.url}")
val attachmentTitle = attachment.title
val id = "${message.id}_${attachment.titleLink}".hashCode().toLong()
return when (attachment) {
is ImageAttachment -> ImageAttachmentViewModel(attachment, message.id, attachmentUrl,
attachmentTitle ?: "", id)
is VideoAttachment -> VideoAttachmentViewModel(attachment, message.id,
attachmentUrl, attachmentTitle ?: "", id)
is AudioAttachment -> AudioAttachmentViewModel(attachment,
message.id, attachmentUrl, attachmentTitle ?: "", id)
else -> null
}
}
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
}
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)
}
}
}
}
val content = getContent(context, message, quote)
MessageViewModel(rawData = message, messageId = message.id,
avatar = avatar!!, time = time, senderName = sender,
content = content.first, isPinned = message.pinned,
isSystemMessage = content.second)
}
private fun getSenderName(message: Message): CharSequence {
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? {
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?): Pair<CharSequence, Boolean> {
var systemMessage = true
val content = when (message.type) {
//TODO: Add implementation for Welcome type.
is MessageType.MessageRemoved -> getSystemMessage(context.getString(R.string.message_removed))
is MessageType.UserJoined -> getSystemMessage(context.getString(R.string.message_user_joined_channel))
is MessageType.UserLeft -> getSystemMessage(context.getString(R.string.message_user_left))
is MessageType.UserAdded -> getSystemMessage(context.getString(R.string.message_user_added_by, message.message, message.sender?.username))
is MessageType.RoomNameChanged -> getSystemMessage(context.getString(R.string.message_room_name_changed, message.message, message.sender?.username))
is MessageType.UserRemoved -> getSystemMessage(context.getString(R.string.message_user_removed_by, message.message, message.sender?.username))
is MessageType.MessagePinned -> getSystemMessage(context.getString(R.string.message_pinned))
else -> {
systemMessage = false
getNormalMessage(message, quote)
}
}
return Pair(content, systemMessage)
}
private suspend fun getNormalMessage(message: Message, quote: Message?): CharSequence {
var quoteViewModel: MessageViewModel? = null
if (quote != null) {
val quoteMessage: Message = quote
quoteViewModel = map(quoteMessage).first { it is MessageViewModel } as MessageViewModel
}
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)
})
}
}
}
\ No newline at end of file
...@@ -2,16 +2,18 @@ package chat.rocket.android.chatrooms.ui ...@@ -2,16 +2,18 @@ package chat.rocket.android.chatrooms.ui
import DateTimeHelper import DateTimeHelper
import android.content.Context import android.content.Context
import android.support.v4.content.res.ResourcesCompat import android.support.v4.content.ContextCompat
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.helper.UrlHelper import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.util.extensions.content
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.textContent import chat.rocket.android.util.extensions.textContent
import chat.rocket.common.model.RoomType
import chat.rocket.core.model.ChatRoom import chat.rocket.core.model.ChatRoom
import com.facebook.drawee.view.SimpleDraweeView import com.facebook.drawee.view.SimpleDraweeView
import kotlinx.android.synthetic.main.avatar.view.* import kotlinx.android.synthetic.main.avatar.view.*
...@@ -28,8 +30,6 @@ class ChatRoomsAdapter(private val context: Context, ...@@ -28,8 +30,6 @@ class ChatRoomsAdapter(private val context: Context,
override fun getItemCount(): Int = dataSet.size override fun getItemCount(): Int = dataSet.size
override fun getItemViewType(position: Int): Int = position
fun updateRooms(newRooms: List<ChatRoom>) { fun updateRooms(newRooms: List<ChatRoom>) {
dataSet.clear() dataSet.clear()
dataSet.addAll(newRooms) dataSet.addAll(newRooms)
...@@ -45,27 +45,40 @@ class ChatRoomsAdapter(private val context: Context, ...@@ -45,27 +45,40 @@ class ChatRoomsAdapter(private val context: Context,
bindUnreadMessages(chatRoom, text_total_unread_messages) bindUnreadMessages(chatRoom, text_total_unread_messages)
if (chatRoom.alert || chatRoom.unread > 0) { if (chatRoom.alert || chatRoom.unread > 0) {
text_chat_name.alpha = 1F text_chat_name.setTextColor(ContextCompat.getColor(context,
text_last_message_date_time.setTextColor(ResourcesCompat.getColor(resources, R.color.colorAccent, null)) R.color.colorPrimaryText))
text_last_message.setTextColor(ResourcesCompat.getColor(resources, android.R.color.primary_text_light, null)) 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) } setOnClickListener { listener(chatRoom) }
} }
private fun bindAvatar(chatRoom: ChatRoom, drawee: SimpleDraweeView) { private fun bindAvatar(chatRoom: ChatRoom, drawee: SimpleDraweeView) {
drawee.setImageURI(UrlHelper.getAvatarUrl(chatRoom.client.url, chatRoom.name)) 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) { private fun bindName(chatRoom: ChatRoom, textView: TextView) {
textView.textContent = chatRoom.name textView.content = chatRoom.name
} }
private fun bindLastMessageDateTime(chatRoom: ChatRoom, textView: TextView) { private fun bindLastMessageDateTime(chatRoom: ChatRoom, textView: TextView) {
val lastMessage = chatRoom.lastMessage val lastMessage = chatRoom.lastMessage
if (lastMessage != null) { if (lastMessage != null) {
val localDateTime = DateTimeHelper.getLocalDateTime(lastMessage.timestamp) val localDateTime = DateTimeHelper.getLocalDateTime(lastMessage.timestamp)
textView.textContent = DateTimeHelper.getDate(localDateTime, context) textView.content = DateTimeHelper.getDate(localDateTime, context)
} else {
textView.content = ""
} }
} }
...@@ -77,16 +90,18 @@ class ChatRoomsAdapter(private val context: Context, ...@@ -77,16 +90,18 @@ class ChatRoomsAdapter(private val context: Context,
val senderUsername = lastMessageSender.username val senderUsername = lastMessageSender.username
when (senderUsername) { when (senderUsername) {
chatRoom.name -> { chatRoom.name -> {
textView.textContent = message textView.content = message
} }
// TODO Change to MySelf // TODO Change to MySelf
// chatRoom.user?.username -> { // chatRoom.user?.username -> {
// holder.lastMessage.textContent = context.getString(R.string.msg_you) + ": $message" // holder.lastMessage.textContent = context.getString(R.string.msg_you) + ": $message"
// } // }
else -> { else -> {
textView.textContent = "@$senderUsername: $message" textView.content = "@$senderUsername: $message"
} }
} }
} else {
textView.content = ""
} }
} }
...@@ -101,6 +116,7 @@ class ChatRoomsAdapter(private val context: Context, ...@@ -101,6 +116,7 @@ class ChatRoomsAdapter(private val context: Context,
textView.textContent = context.getString(R.string.msg_more_than_ninety_nine_unread_messages) textView.textContent = context.getString(R.string.msg_more_than_ninety_nine_unread_messages)
textView.setVisible(true) textView.setVisible(true)
} }
else -> textView.setVisible(false)
} }
} }
} }
......
...@@ -39,6 +39,7 @@ import ru.noties.markwon.il.AsyncDrawableLoader ...@@ -39,6 +39,7 @@ import ru.noties.markwon.il.AsyncDrawableLoader
import ru.noties.markwon.spans.SpannableTheme import ru.noties.markwon.spans.SpannableTheme
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
...@@ -101,6 +102,9 @@ class AppModule { ...@@ -101,6 +102,9 @@ class AppModule {
fun provideOkHttpClient(logger: HttpLoggingInterceptor): OkHttpClient { fun provideOkHttpClient(logger: HttpLoggingInterceptor): OkHttpClient {
return OkHttpClient.Builder().apply { return OkHttpClient.Builder().apply {
addInterceptor(logger) addInterceptor(logger)
connectTimeout(15, TimeUnit.SECONDS)
readTimeout(20, TimeUnit.SECONDS)
writeTimeout(15, TimeUnit.SECONDS)
}.build() }.build()
} }
......
package chat.rocket.android.helper package chat.rocket.android.helper
import android.app.Activity
import android.content.Context
import android.graphics.Rect import android.graphics.Rect
import android.view.View import android.view.View
import android.view.inputmethod.InputMethodManager
object KeyboardHelper { object KeyboardHelper {
...@@ -21,4 +25,29 @@ object KeyboardHelper { ...@@ -21,4 +25,29 @@ object KeyboardHelper {
val heightDiff = rootView.bottom - rect.bottom val heightDiff = rootView.bottom - rect.bottom
return heightDiff > softKeyboardHeight * dm.density 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 package chat.rocket.android.helper
import android.app.Application import android.app.Application
import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.content.Intent
import android.graphics.Color import android.graphics.*
import android.graphics.Paint
import android.graphics.Typeface
import android.graphics.drawable.Drawable 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.support.v4.content.res.ResourcesCompat
import android.text.Layout import android.text.Layout
import android.text.Spannable import android.text.Spannable
import android.text.Spanned import android.text.Spanned
import android.text.TextPaint
import android.text.style.* import android.text.style.*
import android.util.Patterns
import android.view.View import android.view.View
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.viewmodel.MessageViewModel import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import org.commonmark.node.AbstractVisitor
import org.commonmark.node.BlockQuote import org.commonmark.node.BlockQuote
import org.commonmark.node.Text
import ru.noties.markwon.Markwon import ru.noties.markwon.Markwon
import ru.noties.markwon.SpannableBuilder import ru.noties.markwon.SpannableBuilder
import ru.noties.markwon.SpannableConfiguration import ru.noties.markwon.SpannableConfiguration
import ru.noties.markwon.renderer.SpannableMarkdownVisitor import ru.noties.markwon.renderer.SpannableMarkdownVisitor
import timber.log.Timber
import java.util.regex.Pattern import java.util.regex.Pattern
import javax.inject.Inject import javax.inject.Inject
class MessageParser @Inject constructor(val context: Application, private val configuration: SpannableConfiguration) { class MessageParser @Inject constructor(val context: Application, private val configuration: SpannableConfiguration) {
private val parser = Markwon.createParser() private val parser = Markwon.createParser()
private val regexUsername = Pattern.compile("([^\\S]|^)+(@[\\w.]+)", 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@:%_\\+.~#?&/=]*)",
Pattern.MULTILINE or Pattern.CASE_INSENSITIVE) Pattern.MULTILINE or Pattern.CASE_INSENSITIVE)
private val selfReferList = listOf("@all", "@here")
/** /**
* Render a markdown text message to Spannable. * Render a markdown text message to Spannable.
...@@ -43,49 +47,62 @@ class MessageParser @Inject constructor(val context: Application, private val co ...@@ -43,49 +47,62 @@ class MessageParser @Inject constructor(val context: Application, private val co
*/ */
fun renderMarkdown(text: String, quote: MessageViewModel? = null, selfUsername: String? = null): CharSequence { fun renderMarkdown(text: String, quote: MessageViewModel? = null, selfUsername: String? = null): CharSequence {
val builder = SpannableBuilder() val builder = SpannableBuilder()
var content: String = text val content = 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 parentNode = parser.parse(toLenientMarkdown(content)) val parentNode = parser.parse(toLenientMarkdown(content))
parentNode.accept(QuoteMessageBodyVisitor(context, configuration, builder)) parentNode.accept(QuoteMessageBodyVisitor(context, configuration, builder))
quote?.apply { quote?.apply {
var quoteNode = parser.parse("> $senderName $time") var quoteNode = parser.parse("> $senderName $time")
parentNode.appendChild(quoteNode) parentNode.appendChild(quoteNode)
quoteNode.accept(QuoteMessageSenderVisitor(context, configuration, builder, senderName.length)) quoteNode.accept(QuoteMessageSenderVisitor(context, configuration, builder, senderName.length))
quoteNode = parser.parse("> ${toLenientMarkdown(quote.getOriginalMessage())}") quoteNode = parser.parse("> ${toLenientMarkdown(quote.rawData.message)}")
quoteNode.accept(QuoteMessageBodyVisitor(context, configuration, builder)) quoteNode.accept(QuoteMessageBodyVisitor(context, configuration, builder))
} }
parentNode.accept(LinkVisitor(builder))
val result = builder.text() val result = builder.text()
applySpans(result, selfUsername) applySpans(result, selfUsername)
return result return result
} }
private fun applySpans(text: CharSequence, currentUser: String?) { 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 matcher = regexUsername.matcher(text)
val result = text as Spannable val result = text as Spannable
while (matcher.find()) { while (matcher.find()) {
val user = matcher.group(2) val user = matcher.group(2)
val start = matcher.start(2) val start = matcher.start(2)
//TODO: should check if username actually exists prior to applying. //TODO: should check if username actually exists prior to applying.
val linkColor = ResourcesCompat.getColor(context.resources, R.color.white, null) with(context) {
val linkBackgroundColor = ResourcesCompat.getColor(context.resources, R.color.colorAccent, null) val referSelf = when (user) {
val referSelf = currentUser != null && "@$currentUser" == user in selfReferList -> true
val usernameSpan = UsernameClickableSpan(linkBackgroundColor, linkColor, referSelf) "@$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) result.setSpan(usernameSpan, start, start + user.length, 0)
} }
} }
}
/** /**
* Convert to a lenient markdown consistent with Rocket.Chat web markdown instead of the official specs. * Convert to a lenient markdown consistent with Rocket.Chat web markdown instead of the official specs.
...@@ -117,11 +134,51 @@ class MessageParser @Inject constructor(val context: Application, private val co ...@@ -117,11 +134,51 @@ class MessageParser @Inject constructor(val context: Application, private val co
// set time spans // set time spans
builder.setSpan(AbsoluteSizeSpan(res.getDimensionPixelSize(R.dimen.message_time_text_size)), builder.setSpan(AbsoluteSizeSpan(res.getDimensionPixelSize(R.dimen.message_time_text_size)),
timeOffsetStart, builder.length()) timeOffsetStart, builder.length())
builder.setSpan(ForegroundColorSpan(res.getColor(R.color.darkGray)), builder.setSpan(ForegroundColorSpan(ContextCompat.getColor(context, R.color.darkGray)),
timeOffsetStart, builder.length()) timeOffsetStart, builder.length())
} }
} }
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, class QuoteMessageBodyVisitor(private val context: Context,
configuration: SpannableConfiguration, configuration: SpannableConfiguration,
private val builder: SpannableBuilder) : SpannableMarkdownVisitor(configuration, builder) { private val builder: SpannableBuilder) : SpannableMarkdownVisitor(configuration, builder) {
...@@ -134,7 +191,9 @@ class MessageParser @Inject constructor(val context: Application, private val co ...@@ -134,7 +191,9 @@ class MessageParser @Inject constructor(val context: Application, private val co
// pass to super to apply markdown // pass to super to apply markdown
super.visit(blockQuote) 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())
} }
} }
...@@ -174,24 +233,37 @@ class MessageParser @Inject constructor(val context: Application, private val co ...@@ -174,24 +233,37 @@ class MessageParser @Inject constructor(val context: Application, private val co
} }
} }
class UsernameClickableSpan(private val linkBackgroundColor: Int, class MentionSpan(private val backgroundColor: Int,
private val linkTextColor: Int, private val textColor: Int,
private val referSelf: Boolean) : ClickableSpan() { private val radius: Float,
padding: Float,
referSelf: Boolean) : ReplacementSpan() {
private val padding: Float = if (referSelf) padding else 0F
override fun onClick(widget: View) { override fun getSize(paint: Paint,
//TODO: Implement action when clicking on username, like showing user profile. 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) { override fun draw(canvas: Canvas,
if (referSelf) { text: CharSequence,
ds.color = Color.WHITE start: Int,
ds.typeface = Typeface.DEFAULT_BOLD end: Int,
ds.bgColor = linkTextColor x: Float,
} else { top: Int,
ds.color = linkTextColor y: Int,
ds.bgColor = linkBackgroundColor bottom: Int,
} paint: Paint) {
ds.isUnderlineText = false 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)
} }
} }
......
...@@ -83,3 +83,5 @@ fun Map<String, Value<Any>>.uploadMimeTypeFilter(): Array<String> { ...@@ -83,3 +83,5 @@ fun Map<String, Value<Any>>.uploadMimeTypeFilter(): Array<String> {
fun Map<String, Value<Any>>.uploadMaxFileSize(): Int { fun Map<String, Value<Any>>.uploadMaxFileSize(): Int {
return this[UPLOAD_MAX_FILE_SIZE]?.value?.let { it as Int } ?: Int.MAX_VALUE return this[UPLOAD_MAX_FILE_SIZE]?.value?.let { it as Int } ?: Int.MAX_VALUE
} }
fun Map<String, Value<Any>>.baseUrl() : String? = this[SITE_URL]?.value as String
\ No newline at end of file
package chat.rocket.android.util.extensions package chat.rocket.android.util.extensions
import android.text.Spannable
import android.text.Spanned
import android.text.TextUtils
import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.android.widget.emoji.EmojiTypefaceSpan
import ru.noties.markwon.Markwon import ru.noties.markwon.Markwon
fun String.ifEmpty(value: String): String { fun String.ifEmpty(value: String): String {
...@@ -17,6 +23,14 @@ fun CharSequence.ifEmpty(value: String): CharSequence { ...@@ -17,6 +23,14 @@ fun CharSequence.ifEmpty(value: String): CharSequence {
return this return this
} }
fun EditText.erase() {
this.text.clear()
val spans = this.text.getSpans(0, text.length, EmojiTypefaceSpan::class.java)
spans.forEach {
text.removeSpan(it)
}
}
var TextView.textContent: String var TextView.textContent: String
get() = text.toString() get() = text.toString()
set(value) { set(value) {
...@@ -29,12 +43,20 @@ var TextView.hintContent: String ...@@ -29,12 +43,20 @@ var TextView.hintContent: String
hint = value hint = value
} }
var TextView.content: CharSequence var TextView.content: CharSequence?
get() = text get() = text
set(value) { set(value) {
Markwon.unscheduleDrawables(this) Markwon.unscheduleDrawables(this)
Markwon.unscheduleTableRows(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.scheduleDrawables(this)
Markwon.scheduleTableRows(this) Markwon.scheduleTableRows(this)
} }
\ 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 chat.rocket.android.widget.emoji.EmojiKeyboardPopup.Listener
import java.util.*
class CategoryPagerAdapter(val listener: Listener) : 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: Listener) : 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: Listener) : 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
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: Listener? = null
companion object {
const val PREF_EMOJI_RECENTS = "PREF_EMOJI_RECENTS"
}
override fun onCreateView(inflater: LayoutInflater): View {
val view = inflater.inflate(R.layout.emoji_popup_layout, null, false)
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 Listener -> it
else -> {
val fragments = (it as AppCompatActivity).supportFragmentManager.fragments
if (fragments == null || fragments.size == 0 || !(fragments[0] is Listener)) {
throw IllegalStateException("activity/fragment should implement Listener interface")
}
fragments[0] as Listener
}
}
viewPager.adapter = CategoryPagerAdapter(object : Listener {
override fun onNonEmojiKeyPressed(keyCode: Int) {
// do nothing
}
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?.setCustomView(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.setCurrentItem(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) {
}
}
interface Listener {
/**
* 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.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.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
<?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
<?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: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" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:height="24dp"
<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"/> 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>
<?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"?>
<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"?>
<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/tabs"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/pager_categories"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
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="0dp"
android:layout_height="0dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:background="@color/white"
app:layout_constraintBottom_toTopOf="@+id/emoji_actions_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tabs" />
<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" />
<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"?>
<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
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
android:cursorVisible="false" android:cursorVisible="false"
android:hint="@string/default_server" android:hint="@string/default_server"
android:imeOptions="actionDone" android:imeOptions="actionDone"
android:digits="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-/" android:digits="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-/:"
android:inputType="textUri" android:inputType="textUri"
android:paddingEnd="0dp" android:paddingEnd="0dp"
android:paddingStart="0dp" /> android:paddingStart="0dp" />
......
...@@ -31,7 +31,6 @@ ...@@ -31,7 +31,6 @@
style="@style/ChatRoom.Name.TextView" style="@style/ChatRoom.Name.TextView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:alpha="0.6"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/text_last_message_date_time" app:layout_constraintRight_toLeftOf="@+id/text_last_message_date_time"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
......
...@@ -48,20 +48,10 @@ ...@@ -48,20 +48,10 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:layout_marginBottom="2dp"
app:layout_constraintLeft_toLeftOf="@id/top_container" app:layout_constraintLeft_toLeftOf="@id/top_container"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/top_container" 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!" /> 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"
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_constraintTop_toBottomOf="@+id/text_content" />
</android.support.constraint.ConstraintLayout> </android.support.constraint.ConstraintLayout>
\ No newline at end of file
...@@ -5,16 +5,17 @@ ...@@ -5,16 +5,17 @@
android:id="@+id/attachment_container" android:id="@+id/attachment_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="72dp"
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins"
android:orientation="vertical"> android:orientation="vertical">
<com.facebook.drawee.view.SimpleDraweeView <com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_attachment" android:id="@+id/image_attachment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="150dp" android:layout_height="150dp"
android:visibility="gone" android:visibility="visible"
fresco:actualImageScaleType="fitStart" fresco:actualImageScaleType="centerCrop"
fresco:placeholderImage="@drawable/image_dummy" fresco:placeholderImage="@drawable/image_dummy" />
tools:visibility="visible" />
<FrameLayout <FrameLayout
android:id="@+id/audio_video_attachment" android:id="@+id/audio_video_attachment"
...@@ -22,7 +23,7 @@ ...@@ -22,7 +23,7 @@
android:layout_height="150dp" android:layout_height="150dp"
android:background="@color/black" android:background="@color/black"
android:visibility="gone" android:visibility="gone"
tools:visibility="gone"> tools:visibility="visible">
<ImageView <ImageView
android:layout_width="wrap_content" android:layout_width="wrap_content"
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<android.support.constraint.ConstraintLayout
android:id="@+id/composer"
android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<View <View
...@@ -27,46 +33,61 @@ ...@@ -27,46 +33,61 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
android:layout_marginStart="10dp" android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingBottom="10dp" android:paddingBottom="10dp"
android:paddingTop="10dp"
app:layout_constraintTop_toBottomOf="@+id/divider"> app:layout_constraintTop_toBottomOf="@+id/divider">
<ImageButton <ImageButton
android:id="@+id/button_add_reaction" android:id="@+id/button_add_reaction"
android:layout_width="wrap_content" android:layout_width="32dp"
android:layout_height="wrap_content" android:layout_height="32dp"
android:layout_gravity="bottom"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/msg_content_description_show_attachment_options" android:contentDescription="@string/msg_content_description_show_attachment_options"
android:src="@drawable/ic_reaction_24dp" /> android:src="@drawable/ic_reaction_24dp" />
<EditText <chat.rocket.android.widget.emoji.ComposerEditText
android:id="@+id/text_message" android:id="@+id/text_message"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_weight="1" android:layout_weight="1"
android:background="@android:color/transparent" android:background="@android:color/transparent"
android:hint="@string/msg_message" android:hint="@string/msg_message"
android:maxLines="4" /> android:inputType="textCapSentences|textMultiLine"
android:lineSpacingExtra="4dp"
android:maxLines="4"
android:minHeight="24dp"
android:scrollbars="vertical" />
<ImageButton <ImageButton
android:id="@+id/button_show_attachment_options" android:id="@+id/button_show_attachment_options"
android:layout_width="wrap_content" android:layout_width="32dp"
android:layout_height="wrap_content" android:layout_height="32dp"
android:layout_gravity="bottom"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/msg_content_description_show_attachment_options" android:contentDescription="@string/msg_content_description_show_attachment_options"
android:src="@drawable/ic_add_24dp" /> android:src="@drawable/ic_add_24dp" />
<ImageButton <ImageButton
android:id="@+id/button_send" android:id="@+id/button_send"
android:layout_width="wrap_content" android:layout_width="32dp"
android:layout_height="wrap_content" android:layout_height="32dp"
android:layout_gravity="bottom"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/msg_content_description_send_message" android:contentDescription="@string/msg_content_description_send_message"
android:src="@drawable/ic_send_24dp" android:src="@drawable/ic_send_24dp"
android:visibility="gone" /> android:visibility="gone" />
</LinearLayout> </LinearLayout>
</android.support.constraint.ConstraintLayout> </android.support.constraint.ConstraintLayout>
\ No newline at end of file
<FrameLayout
android:id="@+id/emoji_fragment_placeholder"
android:layout_width="match_parent"
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"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/url_preview_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="72dp"
android:layout_marginEnd="24dp">
<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"
tools:text="www.uol.com.br"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/image_preview" />
<TextView
android:id="@+id/text_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textColor="@color/colorAccent"
tools:text="Web page title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/text_host"
app:layout_constraintTop_toBottomOf="@id/text_host"/>
<TextView
android:id="@+id/text_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
tools:text="description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/text_host"
app:layout_constraintTop_toBottomOf="@id/text_title"/>
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
<!--<item--> <!--<item-->
<!--android:id="@+id/action_menu_msg_share"--> <!--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" />--> <!--android:title="@string/action_msg_share" />-->
<item <item
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
<!--<item--> <!--<item-->
<!--android:id="@+id/action_menu_msg_star"--> <!--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" />--> <!--android:title="@string/action_msg_star" />-->
</group> </group>
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
<!-- Text colors --> <!-- Text colors -->
<color name="colorPrimaryText">#DE000000</color> <color name="colorPrimaryText">#DE000000</color>
<color name="colorSecondaryText">#787878</color>
<!-- User status colors --> <!-- User status colors -->
<color name="colorUserStatusOnline">#2FE1A8</color> <color name="colorUserStatusOnline">#2FE1A8</color>
...@@ -27,7 +28,10 @@ ...@@ -27,7 +28,10 @@
<color name="white">#FFFFFFFF</color> <color name="white">#FFFFFFFF</color>
<color name="black">#FF000000</color> <color name="black">#FF000000</color>
<color name="red">#FFFF0000</color> <color name="red">#FFFF0000</color>
<color name="darkGray">#a0a0a0</color> <color name="darkGray">#FFa0a0a0</color>
<color name="actionMenuColor">#727272</color> <color name="actionMenuColor">#FF727272</color>
<color name="whitesmoke">#FFf1f1f1</color>
<color name="colorEmojiIcon">#FF767676</color>
</resources> </resources>
...@@ -14,4 +14,13 @@ ...@@ -14,4 +14,13 @@
<dimen name="message_time_text_size">12sp</dimen> <dimen name="message_time_text_size">12sp</dimen>
<!-- Emoji -->
<dimen name="picker_padding_bottom">16dp</dimen>
<dimen name="supposed_keyboard_height">252dp</dimen>
<!-- Message -->
<dimen name="padding_quote">8dp</dimen>
<dimen name="padding_mention">4dp</dimen>
<dimen name="radius_mention">6dp</dimen>
</resources> </resources>
\ No newline at end of file
...@@ -29,7 +29,6 @@ ext { ...@@ -29,7 +29,6 @@ ext {
markwon : '1.0.3', markwon : '1.0.3',
sheetMenu : '1.3.3', sheetMenu : '1.3.3',
aVLoadingIndicatorView : '2.1.3', aVLoadingIndicatorView : '2.1.3',
swipeBackLayout : '1.1.0',
// For testing // For testing
junit : '4.12', junit : '4.12',
......
/**
* 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