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 {
applicationId "chat.rocket.android"
minSdkVersion 21
targetSdkVersion versions.targetSdk
versionCode 1008
versionName "2.0.0-dev6"
versionCode 1009
versionName "2.0.0-dev7"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}
......@@ -105,8 +105,6 @@ dependencies {
implementation libraries.aVLoadingIndicatorView
implementation libraries.swipeBackLayout
implementation('com.crashlytics.sdk.android:crashlytics:2.6.8@aar') {
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 @@
<activity
android:name=".main.ui.MainActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" />
<activity
android:name=".webview.WebViewActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" />
<activity
android:name=".chatroom.ui.ChatRoomActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" />
<activity
android:name=".chatroom.ui.PinnedMessagesActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" />
<activity
......
This diff is collapsed.
......@@ -9,6 +9,7 @@ import chat.rocket.android.helper.CrashlyticsTree
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.MultiServerTokenRepository
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.widget.emoji.EmojiRepository
import chat.rocket.common.model.Token
import chat.rocket.core.TokenRepository
import com.crashlytics.android.Crashlytics
......@@ -57,6 +58,7 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
initCurrentServer()
AndroidThreeTen.init(this)
EmojiRepository.load(this)
setupCrashlytics()
setupFresco()
......
package chat.rocket.android.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
import android.net.Uri
import chat.rocket.android.R
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.server.domain.*
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
......@@ -34,7 +34,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
private val uriInteractor: UriInteractor,
private val messagesRepository: MessagesRepository,
factory: RocketChatClientFactory,
private val mapper: MessageViewModelMapper) {
private val mapper: ViewModelMapper) {
private val client = factory.create(serverInteractor.get()!!)
private var subId: String? = null
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!!
......@@ -48,7 +48,11 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
client.messages(chatRoomId, roomTypeOf(chatRoomType), offset, 30).result
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)
// Subscribe after getting the first page of messages from REST
......@@ -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) {
client.addStateChannel(stateChannel)
launch(CommonPool + strategy.jobs) {
......@@ -232,9 +247,9 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
is RoomType.Custom -> "custom" //TODO: put appropriate callback string here.
}
view.showReplyingAction(
user,
"[ ](${serverUrl}/${room}/${roomName}?msg=${id}) ${mention} ",
m.message
username = user,
replyMarkdown = "[ ]($serverUrl/$room/$roomName?msg=$id) $mention ",
quotedMessage = m.message
)
}
}
......@@ -315,7 +330,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
private fun updateMessage(streamedMessage: Message) {
launchUI(strategy) {
val viewModelStreamedMessage = mapper.mapToViewModel(streamedMessage, settings)
val viewModelStreamedMessage = mapper.map(streamedMessage)
val roomMessages = messagesRepository.getByRoomId(streamedMessage.roomId)
val index = roomMessages.indexOfFirst { msg -> msg.id == streamedMessage.id }
if (index > -1) {
......
package chat.rocket.android.chatroom.presentation
import android.net.Uri
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
......@@ -12,7 +12,7 @@ interface ChatRoomView : LoadingView, MessageView {
*
* @param dataSet The data set to show.
*/
fun showMessages(dataSet: List<MessageViewModel>)
fun showMessages(dataSet: List<BaseViewModel<*>>)
/**
* Send a message to a chat room.
......@@ -43,7 +43,7 @@ interface ChatRoomView : LoadingView, MessageView {
*
* @param message The (recent) message sent to a chat room.
*/
fun showNewMessage(message: MessageViewModel)
fun showNewMessage(message: List<BaseViewModel<*>>)
/**
* Dispatch to the recycler views adapter that we should remove a message.
......@@ -57,7 +57,7 @@ interface ChatRoomView : LoadingView, MessageView {
*
* @param index The index of the changed message
*/
fun dispatchUpdateMessage(index: Int, message: MessageViewModel)
fun dispatchUpdateMessage(index: Int, message: List<BaseViewModel<*>>)
/**
* Show reply status above the message composer.
......
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.server.domain.GetChatRoomsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
......@@ -18,7 +19,7 @@ class PinnedMessagesPresenter @Inject constructor(private val view: PinnedMessag
private val strategy: CancelStrategy,
private val serverInteractor: GetCurrentServerInteractor,
private val roomsInteractor: GetChatRoomsInteractor,
private val mapper: MessageViewModelMapper,
private val mapper: ViewModelMapper,
factory: RocketChatClientFactory,
getSettingsInteractor: GetSettingsInteractor) {
......@@ -41,8 +42,8 @@ class PinnedMessagesPresenter @Inject constructor(private val view: PinnedMessag
val pinnedMessages =
client.getRoomPinnedMessages(roomId, room.type, pinnedMessagesListOffset)
pinnedMessagesListOffset = pinnedMessages.offset.toInt()
val messageList = mapper.mapToViewModelList(pinnedMessages.result, settings)
.filterNot { it.isSystemMessage }
val messageList = mapper.map(pinnedMessages.result)
.filter { it is MessageViewModel}.filterNot { (it as MessageViewModel).isSystemMessage }
view.showPinnedMessages(messageList)
view.hideLoading()
}.ifNull {
......
package chat.rocket.android.chatroom.presentation
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
......@@ -11,5 +11,5 @@ interface PinnedMessagesView : MessageView, LoadingView {
*
* @param pinnedMessages The list of pinned messages.
*/
fun showPinnedMessages(pinnedMessages: List<MessageViewModel>)
fun showPinnedMessages(pinnedMessages: List<BaseViewModel<*>>)
}
\ No newline at end of file
......@@ -13,8 +13,6 @@ import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector
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
......@@ -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_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only"
class ChatRoomActivity : SwipeBackActivity(), HasSupportFragmentInjector {
class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
private lateinit var chatRoomId: String
private lateinit var chatRoomName: String
......@@ -56,8 +54,6 @@ class ChatRoomActivity : SwipeBackActivity(), HasSupportFragmentInjector {
isChatRoomReadOnly = intent.getBooleanExtra(INTENT_IS_CHAT_ROOM_READ_ONLY, true)
requireNotNull(chatRoomType) { "no is_chat_room_read_only provided in Intent extras" }
swipeBackLayout.setEdgeTrackingEnabled(SwipeBackLayout.EDGE_LEFT)
setupToolbar(chatRoomName)
addFragment("ChatRoomFragment", R.id.fragment_container) {
......@@ -65,7 +61,9 @@ class ChatRoomActivity : SwipeBackActivity(), HasSupportFragmentInjector {
}
}
override fun onBackPressed() = finishActivity()
override fun onBackPressed() {
finishActivity()
}
override fun supportFragmentInjector(): AndroidInjector<Fragment> {
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
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.support.annotation.DrawableRes
import android.support.v4.app.Fragment
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.*
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.ChatRoomAdapter
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
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.KeyboardHelper
import chat.rocket.android.helper.MessageParser
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 io.reactivex.disposables.CompositeDisposable
import kotlinx.android.synthetic.main.fragment_chat_room.*
......@@ -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 REQUEST_CODE_FOR_PERFORM_SAF = 42
class ChatRoomFragment : Fragment(), ChatRoomView {
class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
@Inject lateinit var presenter: ChatRoomPresenter
@Inject lateinit var parser: MessageParser
private lateinit var adapter: ChatRoomAdapter
......@@ -54,6 +61,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
private lateinit var chatRoomId: String
private lateinit var chatRoomName: String
private lateinit var chatRoomType: String
private lateinit var emojiKeyboardPopup: EmojiKeyboardPopup
private var isChatRoomReadOnly: Boolean = false
private lateinit var actionSnackbar: ActionSnackbar
......@@ -98,6 +106,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
setupActionSnackbar()
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
text_message.addTextChangedListener(EmojiKeyboardPopup.EmojiTextWatcher(text_message))
}
override fun onDestroyView() {
presenter.unsubscribeMessages()
handler.removeCallbacksAndMessages(null)
......@@ -132,12 +145,13 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
return true
}
override fun showMessages(dataSet: List<MessageViewModel>) {
override fun showMessages(dataSet: List<BaseViewModel<*>>) {
activity?.apply {
if (recycler_view.adapter == null) {
adapter = ChatRoomAdapter(chatRoomType, chatRoomName, presenter)
recycler_view.adapter = adapter
val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
linearLayoutManager.stackFromEnd = true
recycler_view.layoutManager = linearLayoutManager
recycler_view.itemAnimator = DefaultItemAnimator()
if (dataSet.size >= 30) {
......@@ -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 {
override fun showInvalidFileMessage() = showMessage(getString(R.string.msg_invalid_file))
override fun showNewMessage(message: MessageViewModel) {
adapter.addItem(message)
override fun showNewMessage(message: List<BaseViewModel<*>>) {
adapter.prependData(message)
recycler_view.scrollToPosition(0)
}
......@@ -176,6 +195,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
override fun enableSendMessageButton() {
button_send.isEnabled = true
text_message.isEnabled = true
text_message.erase()
}
override fun clearMessageComposition() {
......@@ -185,8 +206,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
actionSnackbar.dismiss()
}
override fun dispatchUpdateMessage(index: Int, message: MessageViewModel) {
adapter.updateItem(message)
override fun dispatchUpdateMessage(index: Int, message: List<BaseViewModel<*>>) {
adapter.updateItem(message.last())
}
override fun dispatchDeleteMessage(msgId: String) {
......@@ -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>) {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
......@@ -268,11 +311,30 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
input_container.setVisible(false)
} else {
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 {
var message = citation ?: ""
message += text_message.textContent
sendMessage(message)
var textMessage = citation ?: ""
textMessage += text_message.textContent
sendMessage(textMessage)
clearMessageComposition()
}
button_show_attachment_options.setOnClickListener {
......@@ -296,6 +358,31 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
hideAttachmentOptions()
}, 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
import android.view.View
import android.view.ViewGroup
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.ChatRoomAdapter
import chat.rocket.android.chatroom.presentation.PinnedMessagesPresenter
import chat.rocket.android.chatroom.presentation.PinnedMessagesView
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast
......@@ -39,7 +40,7 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
private lateinit var chatRoomId: String
private lateinit var chatRoomName: String
private lateinit var chatRoomType: String
private lateinit var adapter: PinnedMessagesAdapter
private lateinit var adapter: ChatRoomAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
......@@ -71,10 +72,11 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showPinnedMessages(pinnedMessages: List<MessageViewModel>) {
override fun showPinnedMessages(pinnedMessages: List<BaseViewModel<*>>) {
activity?.apply {
if (recycler_view_pinned.adapter == null) {
adapter = PinnedMessagesAdapter()
// TODO - add a better constructor for this case...
adapter = ChatRoomAdapter(chatRoomType, chatRoomName, null, false)
recycler_view_pinned.adapter = adapter
val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
recycler_view_pinned.layoutManager = linearLayoutManager
......@@ -88,7 +90,7 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
}
}
adapter.addDataSet(pinnedMessages)
adapter.appendData(pinnedMessages)
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R
import chat.rocket.core.model.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
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
......@@ -2,16 +2,18 @@ package chat.rocket.android.chatrooms.ui
import DateTimeHelper
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.view.View
import android.view.ViewGroup
import android.widget.TextView
import chat.rocket.android.R
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.setVisible
import chat.rocket.android.util.extensions.textContent
import chat.rocket.common.model.RoomType
import chat.rocket.core.model.ChatRoom
import com.facebook.drawee.view.SimpleDraweeView
import kotlinx.android.synthetic.main.avatar.view.*
......@@ -28,8 +30,6 @@ class ChatRoomsAdapter(private val context: Context,
override fun getItemCount(): Int = dataSet.size
override fun getItemViewType(position: Int): Int = position
fun updateRooms(newRooms: List<ChatRoom>) {
dataSet.clear()
dataSet.addAll(newRooms)
......@@ -45,27 +45,40 @@ class ChatRoomsAdapter(private val context: Context,
bindUnreadMessages(chatRoom, text_total_unread_messages)
if (chatRoom.alert || chatRoom.unread > 0) {
text_chat_name.alpha = 1F
text_last_message_date_time.setTextColor(ResourcesCompat.getColor(resources, R.color.colorAccent, null))
text_last_message.setTextColor(ResourcesCompat.getColor(resources, android.R.color.primary_text_light, null))
text_chat_name.setTextColor(ContextCompat.getColor(context,
R.color.colorPrimaryText))
text_last_message_date_time.setTextColor(ContextCompat.getColor(context,
R.color.colorAccent))
text_last_message.setTextColor(ContextCompat.getColor(context,
android.R.color.primary_text_light))
} else {
text_chat_name.setTextColor(ContextCompat.getColor(context,
R.color.colorSecondaryText))
text_last_message_date_time.setTextColor(ContextCompat.getColor(context,
R.color.colorSecondaryText))
text_last_message.setTextColor(ContextCompat.getColor(context,
R.color.colorSecondaryText))
}
setOnClickListener { listener(chatRoom) }
}
private fun bindAvatar(chatRoom: ChatRoom, 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) {
textView.textContent = chatRoom.name
textView.content = chatRoom.name
}
private fun bindLastMessageDateTime(chatRoom: ChatRoom, textView: TextView) {
val lastMessage = chatRoom.lastMessage
if (lastMessage != null) {
val localDateTime = DateTimeHelper.getLocalDateTime(lastMessage.timestamp)
textView.textContent = DateTimeHelper.getDate(localDateTime, context)
textView.content = DateTimeHelper.getDate(localDateTime, context)
} else {
textView.content = ""
}
}
......@@ -77,16 +90,18 @@ class ChatRoomsAdapter(private val context: Context,
val senderUsername = lastMessageSender.username
when (senderUsername) {
chatRoom.name -> {
textView.textContent = message
textView.content = message
}
// TODO Change to MySelf
// chatRoom.user?.username -> {
// holder.lastMessage.textContent = context.getString(R.string.msg_you) + ": $message"
// }
else -> {
textView.textContent = "@$senderUsername: $message"
textView.content = "@$senderUsername: $message"
}
}
} else {
textView.content = ""
}
}
......@@ -101,6 +116,7 @@ class ChatRoomsAdapter(private val context: Context,
textView.textContent = context.getString(R.string.msg_more_than_ninety_nine_unread_messages)
textView.setVisible(true)
}
else -> textView.setVisible(false)
}
}
}
......
......@@ -39,6 +39,7 @@ import ru.noties.markwon.il.AsyncDrawableLoader
import ru.noties.markwon.spans.SpannableTheme
import timber.log.Timber
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@Module
......@@ -101,6 +102,9 @@ class AppModule {
fun provideOkHttpClient(logger: HttpLoggingInterceptor): OkHttpClient {
return OkHttpClient.Builder().apply {
addInterceptor(logger)
connectTimeout(15, TimeUnit.SECONDS)
readTimeout(20, TimeUnit.SECONDS)
writeTimeout(15, TimeUnit.SECONDS)
}.build()
}
......
package chat.rocket.android.helper
import android.app.Activity
import android.content.Context
import android.graphics.Rect
import android.view.View
import android.view.inputmethod.InputMethodManager
object KeyboardHelper {
......@@ -21,4 +25,29 @@ object KeyboardHelper {
val heightDiff = rootView.bottom - rect.bottom
return heightDiff > softKeyboardHeight * dm.density
}
/**
* Hide the soft keyboard.
*
* @param activity The current focused activity.
*/
fun hideSoftKeyboard(activity: Activity) {
val currentFocus = activity.currentFocus
if (currentFocus != null) {
val inputMethodManager = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(currentFocus.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
}
}
/**
* Show the soft keyboard for the given view.
*
* @param view View to receive input focus.
*/
fun showSoftKeyboard(view: View) {
if (view.requestFocus()) {
val inputMethodManager = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, InputMethodManager.SHOW_IMPLICIT)
}
}
}
\ No newline at end of file
......@@ -83,3 +83,5 @@ fun Map<String, Value<Any>>.uploadMimeTypeFilter(): Array<String> {
fun Map<String, Value<Any>>.uploadMaxFileSize(): Int {
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
import android.text.Spannable
import android.text.Spanned
import android.text.TextUtils
import android.widget.EditText
import android.widget.TextView
import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.android.widget.emoji.EmojiTypefaceSpan
import ru.noties.markwon.Markwon
fun String.ifEmpty(value: String): String {
......@@ -17,6 +23,14 @@ fun CharSequence.ifEmpty(value: String): CharSequence {
return this
}
fun EditText.erase() {
this.text.clear()
val spans = this.text.getSpans(0, text.length, EmojiTypefaceSpan::class.java)
spans.forEach {
text.removeSpan(it)
}
}
var TextView.textContent: String
get() = text.toString()
set(value) {
......@@ -29,12 +43,20 @@ var TextView.hintContent: String
hint = value
}
var TextView.content: CharSequence
var TextView.content: CharSequence?
get() = text
set(value) {
Markwon.unscheduleDrawables(this)
Markwon.unscheduleTableRows(this)
text = value
if (value is Spanned) {
val result = EmojiParser.parse(value.toString()) as Spannable
val end = if (value.length > result.length) result.length else value.length
TextUtils.copySpansFrom(value, 0, end, Any::class.java, result, 0)
text = result
} else {
val result = EmojiParser.parse(value.toString()) as Spannable
text = result
}
Markwon.scheduleDrawables(this)
Markwon.scheduleTableRows(this)
}
\ 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"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
</vector>
<?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
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment