Unverified Commit 62af887c authored by divyanshu bhargava's avatar divyanshu bhargava Committed by GitHub

Merge pull request #2 from RocketChat/develop-2.x

Rocket.chat 2.0 alpha-1
parents 676b84c0 d5fde6a3
......@@ -12,8 +12,8 @@ android {
applicationId "chat.rocket.android"
minSdkVersion 21
targetSdkVersion versions.targetSdk
versionCode 1011
versionName "2.0.0-dev9"
versionCode 2000
versionName "2.0.0-alpha1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}
......@@ -32,7 +32,6 @@ android {
signingConfig signingConfigs.release
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
applicationIdSuffix ".dev"
}
debug {
......@@ -95,14 +94,9 @@ dependencies {
implementation libraries.kotshiApi
implementation libraries.frescoImageViewer
implementation (libraries.androidSvg) {
exclude group: 'org.jetbrains', module: 'annotations-java5'
}
implementation libraries.markwon
implementation (libraries.markwonImageLoader) {
exclude group: 'com.caverock', module: 'androidsvg'
}
implementation libraries.markwonImageLoader
implementation libraries.sheetMenu
......
......@@ -5,6 +5,7 @@ import android.support.v4.graphics.drawable.DrawableCompat
import android.widget.EditText
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.common.model.UserStatus
object DrawableHelper {
......@@ -78,7 +79,7 @@ object DrawableHelper {
* @param drawables The array of Drawable.
* @see compoundDrawable
*/
fun compoundDrawables(textView: Array<EditText>, drawables: Array<Drawable>) {
fun compoundDrawables(textView: Array<TextView>, drawables: Array<Drawable>) {
if (textView.size != drawables.size) {
return
} else {
......@@ -104,15 +105,15 @@ object DrawableHelper {
* @param context The context.
* @return The user status drawable.
*/
fun getUserStatusDrawable(userStatus: String, context: Context): Drawable {
fun getUserStatusDrawable(userStatus: UserStatus, context: Context): Drawable {
val userStatusDrawable = getDrawableFromId(R.drawable.ic_user_status_black, context).mutate()
wrapDrawable(userStatusDrawable)
when (userStatus) {
// TODO: create a enum or check if it will come from the SDK
"online" -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusOnline)
"busy" -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusBusy)
"away" -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusAway)
"offline" -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusOffline)
is UserStatus.Online -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusOnline)
is UserStatus.Busy -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusBusy)
is UserStatus.Away -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusAway)
is UserStatus.Offline -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusOffline)
else -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusOffline)
}
return userStatusDrawable
}
......
package chat.rocket.android.app
data class User(val id: String,
val name: String,
val username: String,
val status: String,
val avatarUri: String)
\ No newline at end of file
package chat.rocket.android.app.utils
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.support.annotation.ColorInt
import android.support.annotation.Nullable
import android.support.v4.graphics.ColorUtils
import com.facebook.common.internal.ByteStreams
import com.facebook.imageformat.ImageFormat
import com.facebook.imageformat.ImageFormatCheckerUtils
import com.facebook.imagepipeline.common.ImageDecodeOptions
import com.facebook.imagepipeline.decoder.ImageDecoder
import com.facebook.imagepipeline.drawable.DrawableFactory
import com.facebook.imagepipeline.image.CloseableImage
import com.facebook.imagepipeline.image.EncodedImage
import com.facebook.imagepipeline.image.QualityInfo
import java.io.IOException
/**
* Simple decoder that can decode color images that have the following format: <color>#FF5722</color>.
*
* @see {https://github.com/facebook/fresco/blob/master/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/imageformat/color/ColorImageExample.java}
*/
object ColorImage {
// Custom ImageFormat for color images.
private val imageFormatColor = ImageFormat("IMAGE_FORMAT_COLOR", "color")
// XML color tag that our colors must start with.
val colorTag = "<color>"
/**
* Creates a new image format checker for [ColorImage.imageFormatColor].
*
* @return the image format checker.
*/
fun createFormatChecker(): ImageFormat.FormatChecker = ColorFormatChecker()
/**
* Creates a new decoder that can decode [ColorImage.imageFormatColor] images.
*
* @return the decoder.
*/
fun createDecoder(): ImageDecoder = ColorDecoder()
fun createDrawableFactory(): ColorDrawableFactory = ColorDrawableFactory()
/**
* Custom color format checker that verifies that the header of the file corresponds to our [ColorImage.colorTag].
*/
class ColorFormatChecker : ImageFormat.FormatChecker {
private val header = ImageFormatCheckerUtils.asciiBytes(colorTag)
override fun getHeaderSize(): Int {
return header.size
}
@Nullable override fun determineFormat(headerBytes: ByteArray, headerSize: Int): ImageFormat? {
if (headerSize > getHeaderSize()) {
if (ImageFormatCheckerUtils.startsWithPattern(headerBytes, header)) {
return imageFormatColor
}
}
return null
}
}
/**
* Custom closeable color image that holds a single color int value.
*/
class CloseableColorImage(@field:ColorInt @get:ColorInt val color: Int) : CloseableImage() {
private var isClosed = false
override fun close() {
isClosed = true
}
override fun getSizeInBytes(): Int = 0
override fun isClosed(): Boolean = isClosed
override fun getWidth(): Int = 0
override fun getHeight(): Int = 0
}
/**
* Decodes a color XML tag: <color>#rrggbb</color>.
*/
class ColorDecoder : ImageDecoder {
@Nullable override fun decode(encodedImage: EncodedImage, length: Int, qualityInfo: QualityInfo, options: ImageDecodeOptions): CloseableImage? {
try {
// Read the file as a string
val text = String(ByteStreams.toByteArray(encodedImage.inputStream))
// Check if the string matches "<color>#"
if (!text.startsWith(colorTag + "#")) {
return null
}
// Parse the int value between # and <
val startIndex = colorTag.length + 1
val endIndex = text.lastIndexOf('<')
var color = Integer.parseInt(text.substring(startIndex, endIndex), 16)
// Add the alpha component so that we actually see the color
color = ColorUtils.setAlphaComponent(color, 255)
// Return the CloseableImage
return CloseableColorImage(color)
} catch (e: IOException) {
// TODO: are we using the android.util.Log for logging that type of errors? or should we use the SDK logger?
e.printStackTrace()
}
// Return nothing if an error occurred
return null
}
}
/**
* Color drawable factory that is able to render a [CloseableColorImage] by creating a new [ColorDrawable] for the given color.
*/
class ColorDrawableFactory : DrawableFactory {
override fun supportsImageType(image: CloseableImage): Boolean {
// We can only handle CloseableColorImages.
return image is CloseableColorImage
}
@Nullable override fun createDrawable(image: CloseableImage): Drawable? {
// Just return a simple ColorDrawable with the given color value.
return ColorDrawable((image as CloseableColorImage).color)
}
}
}
\ No newline at end of file
package chat.rocket.android.app.utils
import com.facebook.drawee.backends.pipeline.DraweeConfig
import com.facebook.imagepipeline.decoder.ImageDecoderConfig
/**
* Utility class to add custom decoders and drawable factories.
*
* @see {https://github.com/facebook/fresco/blob/master/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/CustomImageFormatConfigurator.java}
*/
object CustomImageFormatConfigurator {
fun createImageDecoderConfig() : ImageDecoderConfig {
return ImageDecoderConfig.newBuilder()
.addDecodingCapability(SvgDecoder.svgFormat, SvgDecoder.SvgFormatChecker(), SvgDecoder.Decoder())
.build()
}
fun addCustomDrawableFactories(draweeConfigBuilder: DraweeConfig.Builder) {
// We always add the color drawable factory so that it can be used for image decoder overrides.
draweeConfigBuilder.addCustomDrawableFactory(ColorImage.createDrawableFactory())
draweeConfigBuilder.addCustomDrawableFactory(SvgDecoder.SvgDrawableFactory())
}
}
\ No newline at end of file
package chat.rocket.android.app.utils
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.graphics.drawable.PictureDrawable
import android.support.annotation.Nullable
import com.caverock.androidsvg.SVG
import com.caverock.androidsvg.SVGParseException
import com.facebook.imageformat.ImageFormat
import com.facebook.imageformat.ImageFormatCheckerUtils
import com.facebook.imagepipeline.common.ImageDecodeOptions
import com.facebook.imagepipeline.decoder.ImageDecoder
import com.facebook.imagepipeline.drawable.DrawableFactory
import com.facebook.imagepipeline.image.CloseableImage
import com.facebook.imagepipeline.image.EncodedImage
import com.facebook.imagepipeline.image.QualityInfo
/**
* SVG example that defines all classes required to decode and render SVG images.
*
* @see {https://github.com/facebook/fresco/blob/master/samples/showcase/src/main/java/com/facebook/fresco/samples/showcase/imageformat/svg/SvgDecoderExample.java}
*/
object SvgDecoder {
val svgFormat = ImageFormat("SVG_FORMAT", "svg")
// We do not include the closing ">" since there can be additional information.
private val headerTag = "<svg"
private val possibleHeaderTags = arrayOf(ImageFormatCheckerUtils.asciiBytes("<?xml"))
/**
* Custom SVG format checker that verifies that the header of the file corresponds to our [SvgDecoder.headerTag] or [SvgDecoder.possibleHeaderTags].
*/
class SvgFormatChecker : ImageFormat.FormatChecker {
private val header = ImageFormatCheckerUtils.asciiBytes(headerTag)
override fun getHeaderSize(): Int {
return header.size
}
@Nullable override fun determineFormat(headerBytes: ByteArray, headerSize: Int): ImageFormat? {
if (headerSize > getHeaderSize()) {
if (ImageFormatCheckerUtils.startsWithPattern(headerBytes, header)) {
return svgFormat
}
if (possibleHeaderTags.any { ImageFormatCheckerUtils.startsWithPattern(headerBytes, it) && ImageFormatCheckerUtils.indexOfPattern(headerBytes, headerBytes.size, header, header.size) > -1 }) {
return svgFormat
}
}
return null
}
}
/**
* Custom closeable SVG image that holds a single SVG.
*/
class CloseableSvgImage(val svg: SVG) : CloseableImage() {
private var isClose = false
override fun close() {
isClose = true
}
override fun getSizeInBytes(): Int = 0
override fun isClosed(): Boolean = isClose
override fun getWidth(): Int = 0
override fun getHeight(): Int = 0
}
/**
* Decodes a [SvgDecoder.svgFormat] image.
*/
class Decoder : ImageDecoder {
@Nullable override fun decode(encodedImage: EncodedImage, length: Int, qualityInfo: QualityInfo, options: ImageDecodeOptions): CloseableImage? {
try {
val svg = SVG.getFromInputStream(encodedImage.inputStream)
return CloseableSvgImage(svg)
} catch (e: SVGParseException) {
// TODO: are we using the android.util.Log for logging that type of errors? or should we use the SDK logger?
e.printStackTrace()
}
// Return nothing if an error occurred
return null
}
}
/**
* SVG drawable factory that creates [PictureDrawable]s for SVG images.
*/
class SvgDrawableFactory : DrawableFactory {
override fun supportsImageType(image: CloseableImage): Boolean {
return image is CloseableSvgImage
}
@Nullable override fun createDrawable(image: CloseableImage): Drawable? {
return SvgPictureDrawable((image as CloseableSvgImage).svg)
}
}
class SvgPictureDrawable(private val svg: SVG) : PictureDrawable(null) {
override fun onBoundsChange(bounds: Rect) {
super.onBoundsChange(bounds)
picture = svg.renderToPicture(bounds.width(), bounds.height())
}
}
}
\ No newline at end of file
......@@ -19,12 +19,15 @@ import javax.inject.Inject
class SignupFragment : Fragment(), SignupView {
@Inject lateinit var presenter: SignupPresenter
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
if (KeyboardHelper.isSoftKeyboardShown(constraint_layout.rootView)) {
text_new_user_agreement.setVisible(false)
if (KeyboardHelper.isSoftKeyboardShown(relative_layout.rootView)) {
bottom_container.setVisible(false)
} else {
text_new_user_agreement.setVisible(true)
bottom_container.apply {
postDelayed({
setVisible(true)
}, 3)
}
}
}
......@@ -42,24 +45,22 @@ class SignupFragment : Fragment(), SignupView {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activity?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
tintEditTextDrawableStart()
}
constraint_layout.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
relative_layout.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
setUpNewUserAgreementListener()
button_sign_up.setOnClickListener {
presenter.signup(text_name.textContent, text_username.textContent, text_password.textContent, text_email.textContent)
presenter.signup(text_username.textContent, text_username.textContent, text_password.textContent, text_email.textContent)
}
}
override fun onDestroyView() {
relative_layout.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
super.onDestroyView()
constraint_layout.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
}
override fun alertBlankName() {
......@@ -146,7 +147,7 @@ class SignupFragment : Fragment(), SignupView {
private fun enableUserInput(value: Boolean) {
button_sign_up.isEnabled = value
text_name.isEnabled = value
text_username.isEnabled = value
text_username.isEnabled = value
text_password.isEnabled = value
text_email.isEnabled = value
......
package chat.rocket.android.chatroom.adapter
import android.support.annotation.IntDef
const val PEOPLE = 0L
const val ROOMS = 1L
@Retention(AnnotationRetention.SOURCE)
@IntDef(value = [PEOPLE, ROOMS])
annotation class AutoCompleteType
......@@ -120,7 +120,6 @@ class ChatRoomAdapter(
val indexOfFirst = dataSet.indexOfFirst { it.messageId == message.messageId }
Timber.d("index: $index")
if (index > -1) {
message.nextDownStreamMessage = dataSet[index].nextDownStreamMessage
dataSet[index] = message
notifyItemChanged(index)
while (dataSet[index].nextDownStreamMessage != null) {
......
......@@ -2,6 +2,8 @@ package chat.rocket.android.chatroom.adapter
import android.view.View
import chat.rocket.android.chatroom.viewmodel.ImageAttachmentViewModel
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.drawee.interfaces.DraweeController
import chat.rocket.android.widget.emoji.EmojiReactionListener
import com.stfalcon.frescoimageviewer.ImageViewer
import kotlinx.android.synthetic.main.message_attachment.view.*
......@@ -20,12 +22,21 @@ class ImageAttachmentViewHolder(itemView: View,
override fun bindViews(data: ImageAttachmentViewModel) {
with(itemView) {
image_attachment.setImageURI(data.attachmentUrl)
val controller = Fresco.newDraweeControllerBuilder().apply {
setUri(data.attachmentUrl)
autoPlayAnimations = true
oldController = image_attachment.controller
}.build()
image_attachment.controller = controller
file_name.text = data.attachmentTitle
image_attachment.setOnClickListener { view ->
// TODO - implement a proper image viewer with a proper Transition
val builder = ImageViewer.createPipelineDraweeControllerBuilder()
.setAutoPlayAnimations(true)
ImageViewer.Builder(view.context, listOf(data.attachmentUrl))
.setStartPosition(0)
.hideStatusBar(false)
.setCustomDraweeControllerBuilder(builder)
.show()
}
}
......
package chat.rocket.android.chatroom.adapter
import DrawableHelper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.PeopleSuggestionsAdapter.PeopleSuggestionViewHolder
import chat.rocket.android.chatroom.viewmodel.PeopleViewModel
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.ui.BaseSuggestionViewHolder
import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter
import chat.rocket.common.model.UserStatus
import com.facebook.drawee.view.SimpleDraweeView
class PeopleSuggestionsAdapter : SuggestionsAdapter<PeopleSuggestionViewHolder>("@") {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PeopleSuggestionViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.suggestion_member_item, parent,
false)
return PeopleSuggestionViewHolder(view)
}
class PeopleSuggestionViewHolder(view: View) : BaseSuggestionViewHolder(view) {
override fun bind(item: SuggestionModel, itemClickListener: SuggestionsAdapter.ItemClickListener?) {
item as PeopleViewModel
with(itemView) {
val username = itemView.findViewById<TextView>(R.id.text_username)
val name = itemView.findViewById<TextView>(R.id.text_name)
val avatar = itemView.findViewById<SimpleDraweeView>(R.id.image_avatar)
val statusView = itemView.findViewById<ImageView>(R.id.image_status)
username.text = item.username
name.text = item.name
if (item.imageUri.isEmpty()) {
avatar.setVisible(false)
} else {
avatar.setVisible(true)
avatar.setImageURI(item.imageUri)
}
val status = item.status ?: UserStatus.Offline()
val statusDrawable = DrawableHelper.getUserStatusDrawable(status, itemView.context)
statusView.setImageDrawable(statusDrawable)
setOnClickListener {
itemClickListener?.onClick(item)
}
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.RoomSuggestionsAdapter.RoomSuggestionsViewHolder
import chat.rocket.android.chatroom.viewmodel.ChatRoomViewModel
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.ui.BaseSuggestionViewHolder
import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter
class RoomSuggestionsAdapter : SuggestionsAdapter<RoomSuggestionsViewHolder>("#") {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RoomSuggestionsViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.suggestion_room_item, parent,
false)
return RoomSuggestionsViewHolder(view)
}
class RoomSuggestionsViewHolder(view: View) : BaseSuggestionViewHolder(view) {
override fun bind(item: SuggestionModel, itemClickListener: SuggestionsAdapter.ItemClickListener?) {
item as ChatRoomViewModel
with(itemView) {
val fullname = itemView.findViewById<TextView>(R.id.text_fullname)
val name = itemView.findViewById<TextView>(R.id.text_name)
name.text = item.name
fullname.text = item.fullName
setOnClickListener {
itemClickListener?.onClick(item)
}
}
}
}
}
\ No newline at end of file
......@@ -2,15 +2,23 @@ package chat.rocket.android.chatroom.presentation
import android.net.Uri
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.AutoCompleteType
import chat.rocket.android.chatroom.adapter.PEOPLE
import chat.rocket.android.chatroom.adapter.ROOMS
import chat.rocket.android.chatroom.domain.UriInteractor
import chat.rocket.android.chatroom.viewmodel.ChatRoomViewModel
import chat.rocket.android.chatroom.viewmodel.PeopleViewModel
import chat.rocket.android.chatroom.viewmodel.ViewModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.state
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.UserStatus
import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.realtime.State
......@@ -31,9 +39,13 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
private val strategy: CancelStrategy,
getSettingsInteractor: GetSettingsInteractor,
private val serverInteractor: GetCurrentServerInteractor,
private val getChatRoomsInteractor: GetChatRoomsInteractor,
private val permissions: GetPermissionsInteractor,
private val uriInteractor: UriInteractor,
private val messagesRepository: MessagesRepository,
private val usersRepository: UsersRepository,
private val roomsRepository: RoomRepository,
private val localRepository: LocalRepository,
factory: ConnectionManagerFactory,
private val mapper: ViewModelMapper) {
private val currentServer = serverInteractor.get()!!
......@@ -188,7 +200,8 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
launch(parent = strategy.jobs) {
if (chatRoomId != null && chatRoomType != null) {
val roomType = roomTypeOf(chatRoomType!!)
val lastMessage = messagesRepository.getByRoomId(chatRoomId!!).sortedByDescending { it.timestamp }.first()
messagesRepository.getByRoomId(chatRoomId!!)
.sortedByDescending { it.timestamp }.firstOrNull()?.let { lastMessage ->
val instant = Instant.ofEpochMilli(lastMessage.timestamp)
val messages = client.history(chatRoomId!!, roomType, count = 50,
oldest = instant.toString())
......@@ -210,6 +223,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
}
}
}
}
fun unsubscribeMessages(chatRoomId: String) {
manager.removeStatusChannel(stateChannel)
......@@ -340,8 +354,123 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
}
}
fun loadActiveMembers(chatRoomId: String, chatRoomType: String, offset: Long = 0, filterSelfOut: Boolean = false) {
launchUI(strategy) {
try {
val members = client.getMembers(chatRoomId, roomTypeOf(chatRoomType), offset, 50).result
usersRepository.saveAll(members)
val self = localRepository.get(LocalRepository.USERNAME_KEY)
// Take at most the 100 most recent messages distinguished by user. Can return less.
val recentMessages = messagesRepository.getRecentMessages(chatRoomId, 100)
.filterNot { filterSelfOut && it.sender?.username == self }
val activeUsers = mutableListOf<PeopleViewModel>()
recentMessages.forEach {
val sender = it.sender!!
val username = sender.username ?: ""
val name = sender.name ?: ""
val avatarUrl = UrlHelper.getAvatarUrl(currentServer, username)
val found = members.firstOrNull { member -> member.username == username }
val status = if (found != null) found.status else UserStatus.Offline()
val searchList = mutableListOf(username, name)
activeUsers.add(PeopleViewModel(avatarUrl, username, username, name, status,
true, searchList))
}
// Filter out from members list the active users.
val others = members.filterNot { member ->
activeUsers.firstOrNull {
it.username == member.username
} != null
}
// Add found room members who're not active enough and add them in without pinning.
activeUsers.addAll(others.map {
val username = it.username ?: ""
val name = it.name ?: ""
val avatarUrl = UrlHelper.getAvatarUrl(currentServer, username)
val searchList = mutableListOf(username, name)
PeopleViewModel(avatarUrl, username, username, name, it.status, true, searchList)
})
view.populateMembers(activeUsers)
} catch (e: RocketChatException) {
Timber.e(e)
}
}
}
fun spotlight(query: String, @AutoCompleteType type: Long, filterSelfOut: Boolean = false) {
launchUI(strategy) {
try {
val (users, rooms) = client.spotlight(query)
when (type) {
PEOPLE -> {
if (users.isNotEmpty()) {
usersRepository.saveAll(users)
}
val self = localRepository.get(LocalRepository.USERNAME_KEY)
view.populateMembers(users.map {
val username = it.username ?: ""
val name = it.name ?: ""
val searchList = mutableListOf(username, name)
it.emails?.forEach { email -> searchList.add(email.address) }
PeopleViewModel(UrlHelper.getAvatarUrl(currentServer, username),
username, username, name, it.status, false, searchList)
}.filterNot { filterSelfOut && self != null && self == it.text })
}
ROOMS -> {
if (rooms.isNotEmpty()) {
roomsRepository.saveAll(rooms)
}
view.populateRooms(rooms.map {
val fullName = it.fullName ?: ""
val name = it.name ?: ""
val searchList = mutableListOf(fullName, name)
ChatRoomViewModel(name, fullName, name, searchList)
})
}
}
} catch (e: RocketChatException) {
Timber.e(e)
}
}
}
fun toMembersList(chatRoomId: String, chatRoomType: String) = navigator.toMembersList(chatRoomId, chatRoomType)
fun loadChatRooms() {
launchUI(strategy) {
try {
val chatRooms = getChatRoomsInteractor.get(currentServer)
.filterNot {
it.type is RoomType.DirectMessage || it.type is RoomType.Livechat
}
.map { chatRoom ->
val name = chatRoom.name
val fullName = chatRoom.fullName ?: ""
ChatRoomViewModel(
text = name,
name = name,
fullName = fullName,
searchList = listOf(name, fullName)
)
}
view.populateRooms(chatRooms)
} catch (e: RocketChatException) {
Timber.e(e)
}
}
}
fun joinChat(chatRoomId: String) {
launchUI(strategy) {
try {
client.joinChat(chatRoomId)
view.onJoined()
} catch (ex: RocketChatException) {
Timber.e(ex)
}
}
}
/**
* Send an emoji reaction to a message.
*/
......
......@@ -2,6 +2,8 @@ package chat.rocket.android.chatroom.presentation
import android.net.Uri
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.chatroom.viewmodel.ChatRoomViewModel
import chat.rocket.android.chatroom.viewmodel.PeopleViewModel
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.core.internal.realtime.State
......@@ -100,5 +102,12 @@ interface ChatRoomView : LoadingView, MessageView {
fun showInvalidFileSize(fileSize: Int, maxFileSize: Int)
fun showConnectionState(state: State)
fun populateMembers(members: List<PeopleViewModel>)
fun populateRooms(chatRooms: List<ChatRoomViewModel>)
/**
* This user has joined the chat callback.
*/
fun onJoined()
fun showReactionsPopup(messageId: String)
}
\ No newline at end of file
......@@ -23,13 +23,19 @@ import javax.inject.Inject
import timber.log.Timber
fun Context.chatRoomIntent(chatRoomId: String, chatRoomName: String, chatRoomType: String, isChatRoomReadOnly: Boolean, chatRoomLastSeen: Long): Intent {
fun Context.chatRoomIntent(chatRoomId: String,
chatRoomName: String,
chatRoomType: String,
isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long,
isChatRoomSubscribed: Boolean = true): Intent {
return Intent(this, ChatRoomActivity::class.java).apply {
putExtra(INTENT_CHAT_ROOM_ID, chatRoomId)
putExtra(INTENT_CHAT_ROOM_NAME, chatRoomName)
putExtra(INTENT_CHAT_ROOM_TYPE, chatRoomType)
putExtra(INTENT_IS_CHAT_ROOM_READ_ONLY, isChatRoomReadOnly)
putExtra(INTENT_CHAT_ROOM_LAST_SEEN, chatRoomLastSeen)
putExtra(INTENT_CHAT_IS_SUBSCRIBED, isChatRoomSubscribed)
}
}
......@@ -38,6 +44,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"
private const val INTENT_CHAT_ROOM_LAST_SEEN = "chat_room_last_seen"
private const val INTENT_CHAT_IS_SUBSCRIBED = "is_chat_room_subscribed"
class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
......@@ -50,6 +57,7 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
private lateinit var chatRoomName: String
private lateinit var chatRoomType: String
private var isChatRoomReadOnly: Boolean = false
private var isChatRoomSubscribed: Boolean = true
private var chatRoomLastSeen: Long = -1L
override fun onCreate(savedInstanceState: Bundle?) {
......@@ -76,8 +84,11 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
chatRoomLastSeen = intent.getLongExtra(INTENT_CHAT_ROOM_LAST_SEEN, -1)
isChatRoomSubscribed = intent.getBooleanExtra(INTENT_CHAT_IS_SUBSCRIBED, true)
addFragment("ChatRoomFragment", R.id.fragment_container) {
newInstance(chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly, chatRoomLastSeen)
newInstance(chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly, chatRoomLastSeen,
isChatRoomSubscribed)
}
}
......
......@@ -16,28 +16,36 @@ 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.adapter.PEOPLE
import chat.rocket.android.chatroom.adapter.PeopleSuggestionsAdapter
import chat.rocket.android.chatroom.adapter.RoomSuggestionsAdapter
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.presentation.ChatRoomView
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.chatroom.viewmodel.ChatRoomViewModel
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.chatroom.viewmodel.PeopleViewModel
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.util.extensions.*
import chat.rocket.android.widget.emoji.*
import chat.rocket.core.internal.realtime.State
import chat.rocket.core.model.Message
import dagger.android.support.AndroidSupportInjection
import io.reactivex.disposables.CompositeDisposable
import kotlinx.android.synthetic.main.fragment_chat_room.*
import kotlinx.android.synthetic.main.message_attachment_options.*
import kotlinx.android.synthetic.main.item_chat.*
import kotlinx.android.synthetic.main.message_composer.*
import kotlinx.android.synthetic.main.message_list.*
import timber.log.Timber
import javax.inject.Inject
fun newInstance(chatRoomId: String, chatRoomName: String, chatRoomType: String, isChatRoomReadOnly: Boolean, chatRoomLastSeen: Long): Fragment {
fun newInstance(chatRoomId: String,
chatRoomName: String,
chatRoomType: String,
isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long,
isSubscribed: Boolean = true): Fragment {
return ChatRoomFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
......@@ -45,6 +53,7 @@ fun newInstance(chatRoomId: String, chatRoomName: String, chatRoomType: String,
putString(BUNDLE_CHAT_ROOM_TYPE, chatRoomType)
putBoolean(BUNDLE_IS_CHAT_ROOM_READ_ONLY, isChatRoomReadOnly)
putLong(BUNDLE_CHAT_ROOM_LAST_SEEN, chatRoomLastSeen)
putBoolean(BUNDLE_CHAT_ROOM_IS_SUBSCRIBED, isSubscribed)
}
}
}
......@@ -55,6 +64,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
private const val BUNDLE_CHAT_ROOM_LAST_SEEN = "chat_room_last_seen"
private const val BUNDLE_CHAT_ROOM_IS_SUBSCRIBED = "chat_room_is_subscribed"
class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiReactionListener {
@Inject lateinit var presenter: ChatRoomPresenter
......@@ -63,8 +73,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
private lateinit var chatRoomId: String
private lateinit var chatRoomName: String
private lateinit var chatRoomType: String
private lateinit var emojiKeyboardPopup: EmojiKeyboardPopup
private var isSubscribed: Boolean = true
private var isChatRoomReadOnly: Boolean = false
private lateinit var emojiKeyboardPopup: EmojiKeyboardPopup
private var chatRoomLastSeen: Long = -1
private lateinit var actionSnackbar: ActionSnackbar
private var citation: String? = null
......@@ -90,6 +101,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
chatRoomName = bundle.getString(BUNDLE_CHAT_ROOM_NAME)
chatRoomType = bundle.getString(BUNDLE_CHAT_ROOM_TYPE)
isChatRoomReadOnly = bundle.getBoolean(BUNDLE_IS_CHAT_ROOM_READ_ONLY)
isSubscribed = bundle.getBoolean(BUNDLE_CHAT_ROOM_IS_SUBSCRIBED)
chatRoomLastSeen = bundle.getLong(BUNDLE_CHAT_ROOM_LAST_SEEN)
} else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
......@@ -104,10 +116,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
setupToolbar(chatRoomName)
presenter.loadMessages(chatRoomId, chatRoomType)
presenter.loadChatRooms()
setupRecyclerView()
setupFab()
setupMessageComposer()
setupSuggestionsView()
setupActionSnackbar()
}
......@@ -155,13 +168,13 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
override fun showMessages(dataSet: List<BaseViewModel<*>>) {
// track the message sent immediately after the current message
var prevMessageViewModel : MessageViewModel? = null
var prevMessageViewModel: MessageViewModel? = null
// Loop over received messages to determine first unread
for (i in dataSet.indices) {
val msgModel = dataSet[i]
if (msgModel is MessageViewModel){
if (msgModel is MessageViewModel) {
val msg = msgModel.rawData
if (msg.timestamp < chatRoomLastSeen) {
// This message was sent before the last seen of the room. Hence, it was seen.
......@@ -199,6 +212,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
if (oldMessagesCount == 0 && dataSet.isNotEmpty()) {
recycler_view.scrollToPosition(0)
}
presenter.loadActiveMembers(chatRoomId, chatRoomType, filterSelfOut = true)
}
}
......@@ -273,6 +287,14 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun populateMembers(members: List<PeopleViewModel>) {
suggestions_view.addItems("@", members)
}
override fun populateRooms(chatRooms: List<ChatRoomViewModel>) {
suggestions_view.addItems("#", chatRooms)
}
override fun copyToClipboard(message: String) {
activity?.apply {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
......@@ -362,6 +384,13 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}
}
override fun onJoined() {
input_container.setVisible(true)
button_join_chat.setVisible(false)
isSubscribed = true
setupMessageComposer()
}
private val dismissStatus = {
connection_status_text.fadeOut()
}
......@@ -392,6 +421,10 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
if (isChatRoomReadOnly) {
text_room_is_read_only.setVisible(true)
input_container.setVisible(false)
} else if (!isSubscribed) {
input_container.setVisible(false)
button_join_chat.setVisible(true)
button_join_chat.setOnClickListener { presenter.joinChat(chatRoomId) }
} else {
button_send.alpha = 0f
button_send.setVisible(false)
......@@ -458,14 +491,30 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}
}
private fun setupSuggestionsView() {
suggestions_view.anchor(text_message)
.bindTokenAdapter(PeopleSuggestionsAdapter())
.bindTokenAdapter(RoomSuggestionsAdapter())
.addSuggestionProviderAction("@") { query ->
if (query.isNotEmpty()) {
presenter.spotlight(query, PEOPLE, true)
}
}
.addSuggestionProviderAction("#") { query ->
if (query.isNotEmpty()) {
presenter.loadChatRooms()
}
}
}
private fun openEmojiKeyboardPopup() {
if (!emojiKeyboardPopup.isShowing()) {
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.isFocusableInTouchMode = true
text_message.requestFocus()
emojiKeyboardPopup.showAtBottomPending()
KeyboardHelper.showSoftKeyboard(text_message)
......
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
class ChatRoomViewModel(text: String,
val fullName: String,
val name: String,
searchList: List<String>) : SuggestionModel(text, searchList, false) {
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.common.model.UserStatus
class PeopleViewModel(val imageUri: String,
text: String,
val username: String,
val name: String,
val status: UserStatus?,
pinned: Boolean = false,
searchList: List<String>) : SuggestionModel(text, searchList, pinned) {
override fun toString(): String {
return "PeopleViewModel(imageUri='$imageUri', username='$username', name='$name', status=$status, pinned=$pinned)"
}
}
\ No newline at end of file
......@@ -96,31 +96,53 @@ class ViewModelMapper @Inject constructor(private val context: Context,
}
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()
val attachmentUrl = attachmentUrl(attachment)
val attachmentTitle = attachmentTitle(attachment)
val id = attachmentId(message, attachment)
return when (attachment) {
is ImageAttachment -> ImageAttachmentViewModel(message, attachment, message.id,
attachmentUrl, attachmentTitle ?: "", id, getReactions(message))
attachmentUrl, attachmentTitle, id, getReactions(message))
is VideoAttachment -> VideoAttachmentViewModel(message, attachment, message.id,
attachmentUrl, attachmentTitle ?: "", id, getReactions(message))
attachmentUrl, attachmentTitle, id, getReactions(message))
is AudioAttachment -> AudioAttachmentViewModel(message, attachment, message.id,
attachmentUrl, attachmentTitle ?: "", id, getReactions(message))
attachmentUrl, attachmentTitle, id, getReactions(message))
else -> null
}
}
private fun attachmentUrl(url: String): String {
var response = url
val httpUrl = HttpUrl.parse(url)
private fun attachmentId(message: Message, attachment: FileAttachment): Long {
return "${message.id}_${attachment.url}".hashCode().toLong()
}
private fun attachmentTitle(attachment: FileAttachment): CharSequence {
return with(attachment) {
title?.let { return@with it }
val fileUrl = HttpUrl.parse(url)
fileUrl?.let {
return@with it.pathSegments().last()
}
return@with ""
}
}
private fun attachmentUrl(attachment: FileAttachment): String {
return with(attachment) {
if (url.startsWith("http")) return@with url
val fullUrl = "$baseUrl$url"
val httpUrl = HttpUrl.parse(fullUrl)
httpUrl?.let {
response = it.newBuilder().apply {
return@with it.newBuilder().apply {
addQueryParameter("rc_uid", token?.userId)
addQueryParameter("rc_token", token?.authToken)
}.build().toString()
}
return response
// Fallback to baseUrl + url
return@with fullUrl
}
}
private suspend fun mapMessage(message: Message): MessageViewModel = withContext(CommonPool) {
......
......@@ -9,12 +9,12 @@ import chat.rocket.android.server.infraestructure.chatRooms
import chat.rocket.android.server.infraestructure.state
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.BaseRoom
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.*
import chat.rocket.core.internal.model.Subscription
import chat.rocket.core.internal.realtime.State
import chat.rocket.core.internal.realtime.StreamMessage
import chat.rocket.core.internal.realtime.Type
import chat.rocket.core.internal.rest.spotlight
import chat.rocket.core.model.ChatRoom
import chat.rocket.core.model.Room
import kotlinx.coroutines.experimental.*
......@@ -34,6 +34,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
factory: ConnectionManagerFactory) {
private val manager: ConnectionManager = factory.create(serverInteractor.get()!!)
private val currentServer = serverInteractor.get()!!
private val client = manager.client
private var reloadJob: Deferred<List<ChatRoom>>? = null
private val settings = settingsRepository.get(currentServer)!!
......@@ -69,7 +70,9 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
}
navigator.toChatRoom(chatRoom.id, roomName,
chatRoom.type.toString(), chatRoom.readonly ?: false, chatRoom.lastSeen ?: -1)
chatRoom.type.toString(), chatRoom.readonly ?: false,
chatRoom.lastSeen ?: -1,
chatRoom.open)
}
/**
......@@ -79,9 +82,42 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
fun chatRoomsByName(name: String) {
val currentServer = serverInteractor.get()!!
launchUI(strategy) {
try {
val roomList = getChatRoomsInteractor.getByName(currentServer, name)
if (roomList.isEmpty()) {
val (users, rooms) = client.spotlight(name)
val chatRoomsCombined = mutableListOf<ChatRoom>()
chatRoomsCombined.addAll(usersToChatRooms(users))
chatRoomsCombined.addAll(roomsToChatRooms(rooms))
view.updateChatRooms(chatRoomsCombined)
} else {
view.updateChatRooms(roomList)
}
} catch (ex: RocketChatException) {
Timber.e(ex)
}
}
}
private suspend fun usersToChatRooms(users: List<User>): List<ChatRoom> {
return users.map {
ChatRoom(it.id, RoomType.DIRECT_MESSAGE, SimpleUser(
username = it.username, name = it.name, id = null), it.name ?: "",
it.name, false, null, null, null,
null, null, false, false, false,
0L, null, 0L, null, client
)
}
}
private suspend fun roomsToChatRooms(rooms: List<Room>): List<ChatRoom> {
return rooms.map {
ChatRoom(it.id, it.type, it.user, it.name ?: "",
it.fullName, it.readonly, it.updatedAt, null, null,
it.topic, it.announcement, false, false, false,
0L, null, 0L, it.lastMessage, client
)
}
}
private suspend fun loadRooms(): List<ChatRoom> {
......
......@@ -134,8 +134,10 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
}
private val dismissStatus = {
if (connection_status_text != null) {
connection_status_text.fadeOut()
}
}
private fun setupToolbar() {
(activity as AppCompatActivity).supportActionBar?.title = getString(R.string.title_chats)
......
......@@ -7,7 +7,6 @@ import android.content.SharedPreferences
import chat.rocket.android.BuildConfig
import chat.rocket.android.R
import chat.rocket.android.app.RocketChatDatabase
import chat.rocket.android.app.utils.CustomImageFormatConfigurator
import chat.rocket.android.authentication.infraestructure.MemoryTokenRepository
import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository
import chat.rocket.android.dagger.qualifier.ForFresco
......@@ -131,7 +130,6 @@ class AppModule {
listeners.add(RequestLoggingListener())
return OkHttpImagePipelineConfigFactory.newBuilder(context, okHttpClient)
.setImageDecoderConfig(CustomImageFormatConfigurator.createImageDecoderConfig())
.setRequestListeners(listeners)
.setDownsampleEnabled(true)
//.experiment().setBitmapPrepareToDraw(true).experiment()
......@@ -141,11 +139,7 @@ class AppModule {
@Provides
@Singleton
fun provideDraweeConfig(): DraweeConfig {
val draweeConfigBuilder = DraweeConfig.newBuilder()
CustomImageFormatConfigurator.addCustomDrawableFactories(draweeConfigBuilder)
return draweeConfigBuilder.build()
return DraweeConfig.newBuilder().build()
}
@Provides
......@@ -185,7 +179,13 @@ class AppModule {
@Provides
@Singleton
fun provideChatRoomsRepository(): ChatRoomsRepository {
fun provideRoomRepository(): RoomRepository {
return MemoryRoomRepository()
}
@Provides
@Singleton
fun provideChatRoomRepository(): ChatRoomsRepository {
return MemoryChatRoomsRepository()
}
......@@ -207,6 +207,12 @@ class AppModule {
return MemoryMessagesRepository()
}
@Provides
@Singleton
fun provideUserRepository(): UsersRepository {
return MemoryUsersRepository()
}
@Provides
@Singleton
fun provideConfiguration(context: Application, client: OkHttpClient): SpannableConfiguration {
......
......@@ -11,7 +11,8 @@ object UrlHelper {
* @param avatarName The avatar name.
* @return The avatar URL.
*/
fun getAvatarUrl(serverUrl: String, avatarName: String): String = removeTrailingSlash(serverUrl) + "/avatar/" + avatarName
fun getAvatarUrl(serverUrl: String, avatarName: String, format: String = "jpeg"): String =
removeTrailingSlash(serverUrl) + "/avatar/" + removeTrailingSlash(avatarName) + "?format=$format"
/**
* Returns the CAS URL.
......
......@@ -29,8 +29,14 @@ class MainNavigator(internal val activity: MainActivity, internal val context: C
}
}
fun toChatRoom(chatRoomId: String, chatRoomName: String, chatRoomType: String, isChatRoomReadOnly: Boolean, chatRoomLastSeen: Long) {
activity.startActivity(context.chatRoomIntent(chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly, chatRoomLastSeen))
fun toChatRoom(chatRoomId: String,
chatRoomName: String,
chatRoomType: String,
isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long,
isChatRoomSubscribed: Boolean) {
activity.startActivity(context.chatRoomIntent(chatRoomId, chatRoomName, chatRoomType,
isChatRoomReadOnly, chatRoomLastSeen, isChatRoomSubscribed))
activity.overridePendingTransition(R.anim.open_enter, R.anim.open_exit)
}
}
\ No newline at end of file
......@@ -15,6 +15,7 @@ import chat.rocket.android.members.adapter.MembersAdapter
import chat.rocket.android.members.presentation.MembersPresenter
import chat.rocket.android.members.presentation.MembersView
import chat.rocket.android.members.viewmodel.MemberViewModel
import chat.rocket.android.util.extensions.hideKeyboard
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast
......@@ -22,6 +23,10 @@ import chat.rocket.android.widget.DividerItemDecoration
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_members.*
import javax.inject.Inject
import android.view.inputmethod.InputMethodManager.HIDE_IMPLICIT_ONLY
import android.app.Activity
import android.view.inputmethod.InputMethodManager
fun newInstance(chatRoomId: String, chatRoomType: String): Fragment {
return MembersFragment().apply {
......@@ -60,6 +65,9 @@ class MembersFragment : Fragment(), MembersView {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val imm = activity?.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
imm.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0)
(activity as AppCompatActivity).supportActionBar?.title = ""
setupRecyclerView()
......
......@@ -25,7 +25,7 @@ class MemberViewModel(private val member: User, private val settings: Map<String
private fun getUserAvatar(): String? {
val username = member.username ?: "?"
return baseUrl?.let {
UrlHelper.getAvatarUrl(baseUrl, username)
UrlHelper.getAvatarUrl(baseUrl, username, "png")
}
}
......
......@@ -13,7 +13,7 @@ import chat.rocket.core.internal.rest.setAvatar
import chat.rocket.core.internal.rest.updateProfile
import javax.inject.Inject
class ProfilePresenter @Inject constructor (private val view: ProfileView,
class ProfilePresenter @Inject constructor(private val view: ProfileView,
private val strategy: CancelStrategy,
serverInteractor: GetCurrentServerInteractor,
factory: RocketChatClientFactory) {
......@@ -30,8 +30,8 @@ class ProfilePresenter @Inject constructor (private val view: ProfileView,
val avatarUrl = UrlHelper.getAvatarUrl(serverUrl, myself.username!!)
view.showProfile(
avatarUrl,
myself.name!!,
myself.username!!,
myself.name ?: "",
myself.username ?: "",
myself.emails?.get(0)?.address!!
)
} catch (exception: RocketChatException) {
......
......@@ -56,8 +56,8 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
text_email.textContent = email
text_avatar_url.textContent = ""
currentName = name
currentUsername = username
currentName = username
currentUsername = name
currentEmail = email
currentAvatar = avatarUrl
......@@ -133,10 +133,10 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
text_email.asObservable(),
text_avatar_url.asObservable()) { text_name, text_username, text_email, text_avatar_url ->
return@combineLatest (text_name.toString() != currentName ||
text_username.toString() !=currentUsername ||
text_username.toString() != currentUsername ||
text_email.toString() != currentEmail ||
(text_avatar_url.toString() != "" && text_avatar_url.toString()!= currentAvatar))
}.subscribe({ isValid->
(text_avatar_url.toString() != "" && text_avatar_url.toString() != currentAvatar))
}.subscribe({ isValid ->
if (isValid) {
startActionMode()
} else {
......@@ -154,7 +154,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
private fun finishActionMode() = actionMode?.finish()
private fun enableUserInput(value: Boolean) {
text_name.isEnabled = value
text_username.isEnabled = value
text_username.isEnabled = value
text_email.isEnabled = value
text_avatar_url.isEnabled = value
......
......@@ -6,6 +6,14 @@ import kotlinx.coroutines.experimental.withContext
import javax.inject.Inject
class GetChatRoomsInteractor @Inject constructor(private val repository: ChatRoomsRepository) {
/**
* Get all ChatRoom objects.
*
* @param url The server url.
*
* @return All the ChatRoom objects.
*/
fun get(url: String) = repository.get(url)
/**
......
......@@ -8,6 +8,7 @@ interface MessagesRepository {
* Get message by its message id.
*
* @param id The id of the message to get.
*
* @return The Message object given by the id or null if message wasn't found.
*/
fun getById(id: String): Message?
......@@ -20,8 +21,19 @@ interface MessagesRepository {
*/
fun getByRoomId(rid: String): List<Message>
/**
* Get most recent messages up to count different users.
*
* @param rid The id of the room the messages are.
* @param count The count last messages to get.
*
* @return List of last count messages.
*/
fun getRecentMessages(rid: String, count: Long): List<Message>
/**
* Get all messages. Use carefully!
*
* @return All messages or an empty list.
*/
fun getAll(): List<Message>
......
package chat.rocket.android.server.domain
import chat.rocket.common.model.RoomType
import chat.rocket.core.model.Room
interface RoomRepository {
/**
* Get all rooms. Use carefully!
*
* @return All rooms or an empty list.
*/
fun getAll(): List<Room>
fun get(query: Query.() -> Unit): List<Room>
/**
* Save a single room object.
*
* @param room The room object to save.
*/
fun save(room: Room)
/**
* Save a list of rooms.
*
* @param roomList The list of rooms to save.
*/
fun saveAll(roomList: List<Room>)
/**
* Removes all rooms.
*/
fun clear()
data class Query(
var id: String? = null,
var name: String? = null,
var fullName: String? = null,
var type: RoomType? = null,
var readonly: Boolean? = null
)
}
\ No newline at end of file
package chat.rocket.android.server.domain
import chat.rocket.common.model.Email
import chat.rocket.common.model.User
import chat.rocket.common.model.UserStatus
interface UsersRepository {
/**
* Get all users. Use carefully!
*
* @return All users or an empty list.
*/
fun getAll(): List<User>
fun get(query: Query.() -> Unit): List<User>
/**
* Save a single user object.
*
* @param user The user object to save.
*/
fun save(user: User)
/**
* Save a list of users.
*
* @param users The list of users to save.
*/
fun saveAll(userList: List<User>)
/**
* Removes all users.
*/
fun clear()
data class Query(
var id: String? = null,
var name: String? = null,
var username: String? = null,
var emails: List<Email>? = null,
var utfOffset: Float? = null,
var status: UserStatus? = null,
var limit: Long = 0L
)
}
\ No newline at end of file
......@@ -15,6 +15,11 @@ class MemoryMessagesRepository : MessagesRepository {
return messages.filter { it.value.roomId == rid }.values.toList()
}
override fun getRecentMessages(rid: String, count: Long): List<Message> {
return getByRoomId(rid).sortedByDescending { it.timestamp }
.distinctBy { it.sender }.take(count.toInt())
}
override fun getAll(): List<Message> = messages.values.toList()
override fun save(message: Message) {
......
package chat.rocket.android.server.infraestructure
import chat.rocket.android.server.domain.RoomRepository
import chat.rocket.android.server.domain.RoomRepository.Query
import chat.rocket.core.model.Room
import java.util.concurrent.CopyOnWriteArrayList
class MemoryRoomRepository : RoomRepository {
private val rooms = CopyOnWriteArrayList<Room>()
override fun getAll() = rooms.toList()
override fun get(query: Query.() -> Unit): List<Room> {
val q = Query().apply(query)
return rooms.filter {
with(q) {
if (name != null && it.name?.contains(name!!.toRegex()) == true) return@filter false
if (fullName != null && it.fullName?.contains(fullName!!.toRegex()) == true) return@filter false
if (id != null && id == it.id) return@filter false
if (readonly != null && readonly == it.readonly) return@filter false
if (type != null && type == it.type) return@filter false
return@filter true
}
}
}
override fun save(room: Room) {
rooms.addIfAbsent(room)
}
override fun saveAll(roomList: List<Room>) {
rooms.addAllAbsent(roomList)
}
override fun clear() {
rooms.clear()
}
}
\ No newline at end of file
package chat.rocket.android.server.infraestructure
import chat.rocket.android.server.domain.UsersRepository
import chat.rocket.android.server.domain.UsersRepository.Query
import chat.rocket.common.model.User
import java.util.concurrent.CopyOnWriteArrayList
class MemoryUsersRepository : UsersRepository {
private val users = CopyOnWriteArrayList<User>()
override fun getAll(): List<User> {
return users.toList()
}
override fun get(query: Query.() -> Unit): List<User> {
val q = Query().apply(query)
return users.filter {
with(q) {
if (name != null && it.name?.contains(name!!.toRegex()) == true) return@filter false
if (username != null && it.username?.contains(username!!.toRegex()) == true) return@filter false
if (id != null && id == it.id) return@filter false
if (status != null && status == it.status) return@filter false
return@filter true
}
}
}
override fun save(user: User) {
users.addIfAbsent(user)
}
override fun saveAll(userList: List<User>) {
users.addAllAbsent(userList)
}
override fun clear() {
this.users.clear()
}
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.model
abstract class SuggestionModel(val text: String, // This is the text key for searches, must be unique.
val searchList: List<String> = emptyList(), // Where to search for matches.
val pinned: Boolean = false /* If pinned item will have priority to show */) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SuggestionModel) return false
if (text != other.text) return false
return true
}
override fun hashCode(): Int {
return text.hashCode()
}
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.repository
interface LocalSuggestionProvider {
fun find(prefix: String)
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.strategy
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
interface CompletionStrategy {
fun getItem(prefix: String, position: Int): SuggestionModel
fun autocompleteItems(prefix: String): List<SuggestionModel>
fun addAll(list: List<SuggestionModel>)
fun size(): Int
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.strategy.regex
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.strategy.CompletionStrategy
import java.util.concurrent.CopyOnWriteArrayList
internal class StringMatchingCompletionStrategy : CompletionStrategy {
private val list = CopyOnWriteArrayList<SuggestionModel>()
override fun autocompleteItems(prefix: String): List<SuggestionModel> {
return list.filter {
it.searchList.forEach { word ->
if (word.contains(prefix, ignoreCase = true)) {
return@filter true
}
}
false
}.sortedByDescending { it.pinned }.take(5)
}
override fun addAll(list: List<SuggestionModel>) {
// this.list.removeAll { !it.pinned }
this.list.addAllAbsent(list)
}
override fun getItem(prefix: String, position: Int): SuggestionModel {
return list[position]
}
override fun size(): Int {
return list.size
}
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.strategy.trie
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.strategy.CompletionStrategy
import chat.rocket.android.widget.autocompletion.strategy.trie.data.Trie
class TrieCompletionStrategy : CompletionStrategy {
private val items = mutableListOf<SuggestionModel>()
private val trie = Trie()
override fun getItem(prefix: String, position: Int): SuggestionModel {
val item: SuggestionModel
if (prefix.isEmpty()) {
item = items[position]
} else {
item = autocompleteItems(prefix)[position]
}
return item
}
override fun autocompleteItems(prefix: String) = trie.autocompleteItems(prefix)
override fun addAll(list: List<SuggestionModel>) {
items.addAll(list)
list.forEach {
trie.insert(it)
}
}
override fun size() = items.size
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.strategy.trie.data
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
internal class Trie {
private val root = TrieNode(' ')
private var count = 0
fun insert(item: SuggestionModel) {
val sanitizedWord = item.text.trim().toLowerCase()
// Word exists, bail out.
if (search(sanitizedWord)) return
var current = root
sanitizedWord.forEach { ch ->
val child = current.getChild(ch)
if (child == null) {
val node = TrieNode(ch, current)
current.children[ch] = node
current = node
count++
} else {
current = child
}
}
// Set last node as leaf.
if (current != root) {
current.isLeaf = true
current.item = item
}
}
fun search(word: String): Boolean {
val sanitizedWord = word.trim().toLowerCase()
var current = root
sanitizedWord.forEach { ch ->
val child = current.getChild(ch)
if (child == null) {
return false
}
current = child
}
if (current.isLeaf) {
return true
}
return false
}
fun autocomplete(prefix: String): List<String> {
val sanitizedPrefix = prefix.trim().toLowerCase()
var lastNode: TrieNode? = root
sanitizedPrefix.forEach { ch ->
lastNode = lastNode?.getChild(ch)
if (lastNode == null) return emptyList()
}
return lastNode!!.getWords()
}
fun autocompleteItems(prefix: String): List<SuggestionModel> {
val sanitizedPrefix = prefix.trim().toLowerCase()
var lastNode: TrieNode? = root
sanitizedPrefix.forEach { ch ->
lastNode = lastNode?.getChild(ch)
if (lastNode == null) return emptyList()
}
return lastNode!!.getItems()
}
fun getCount() = count
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.strategy.trie.data
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
internal class TrieNode(internal var data: Char,
internal var parent: TrieNode? = null,
internal var isLeaf: Boolean = false,
internal var item: SuggestionModel? = null) {
val children = hashMapOf<Char, TrieNode>()
fun getChild(c: Char): TrieNode? {
children.forEach {
if (it.key == c) return it.value
}
return null
}
fun getWords(): List<String> {
val list = arrayListOf<String>()
if (isLeaf) {
list.add(toString())
}
children.forEach { node ->
node.value.let {
list.addAll(it.getWords())
}
}
return list
}
class X : SuggestionModel("")
fun getItems(): List<SuggestionModel> {
val list = arrayListOf<SuggestionModel>()
if (isLeaf) {
list.add(item!!)
}
children.forEach { node ->
node.value.let {
list.addAll(it.getItems())
}
}
return list
}
override fun toString(): String = if (parent == null) "" else "${parent.toString()}$data"
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.ui
import android.support.v7.widget.RecyclerView
import android.view.View
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
abstract class BaseSuggestionViewHolder(view: View) : RecyclerView.ViewHolder(view) {
abstract fun bind(item: SuggestionModel, itemClickListener: SuggestionsAdapter.ItemClickListener?)
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.ui
import android.content.Context
import android.support.v7.widget.RecyclerView
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.view.WindowManager
import chat.rocket.android.R
internal class PopupRecyclerView : RecyclerView {
private var displayWidth: Int = 0
constructor(context: Context?) : this(context, null)
constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) {
val wm = context!!.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val display = wm.defaultDisplay
val size = DisplayMetrics()
display.getMetrics(size)
val screenWidth = size.widthPixels
displayWidth = screenWidth
}
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
val hSpec = MeasureSpec.makeMeasureSpec(resources.getDimensionPixelSize(
R.dimen.popup_max_height), MeasureSpec.AT_MOST)
val wSpec = MeasureSpec.makeMeasureSpec(displayWidth, MeasureSpec.EXACTLY)
super.onMeasure(wSpec, hSpec)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
super.onLayout(changed, l + 40, t, r - 40, b)
}
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.ui
import android.support.v7.widget.RecyclerView
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.strategy.CompletionStrategy
import chat.rocket.android.widget.autocompletion.strategy.regex.StringMatchingCompletionStrategy
import java.lang.reflect.Type
import kotlin.properties.Delegates
abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>(val token: String) : RecyclerView.Adapter<VH>() {
private val strategy: CompletionStrategy = StringMatchingCompletionStrategy()
private var itemType: Type? = null
private var itemClickListener: ItemClickListener? = null
private var providerExternal: ((query: String) -> Unit)? = null
private var prefix: String by Delegates.observable("", { _, _, _ ->
strategy.autocompleteItems(prefix)
notifyItemRangeChanged(0, 5)
})
init {
setHasStableIds(true)
}
override fun getItemId(position: Int): Long {
return getItem(position).text.hashCode().toLong()
}
override fun onBindViewHolder(holder: VH, position: Int) {
holder.bind(getItem(position), itemClickListener)
}
override fun getItemCount() = strategy.autocompleteItems(prefix).size
private fun getItem(position: Int): SuggestionModel {
return strategy.autocompleteItems(prefix)[position]
}
fun autocomplete(prefix: String) {
this.prefix = prefix.toLowerCase().trim()
}
fun addItems(list: List<SuggestionModel>) {
strategy.addAll(list)
// Since we've just added new items we should check for possible new completion suggestions.
strategy.autocompleteItems(prefix)
notifyItemRangeChanged(0, 5)
}
fun setOnClickListener(clickListener: ItemClickListener) {
this.itemClickListener = clickListener
}
fun hasItemClickListener() = itemClickListener != null
fun prefix() = prefix
fun cancel() {
strategy.addAll(emptyList())
strategy.autocompleteItems(prefix)
notifyDataSetChanged()
}
interface ItemClickListener {
fun onClick(item: SuggestionModel)
}
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.ui
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.support.annotation.DrawableRes
import android.support.transition.Slide
import android.support.transition.TransitionManager
import android.support.v4.content.ContextCompat
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.text.Editable
import android.text.InputType
import android.text.TextWatcher
import android.util.AttributeSet
import android.view.Gravity
import android.view.View
import android.widget.EditText
import android.widget.FrameLayout
import chat.rocket.android.R
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import java.lang.ref.WeakReference
import java.util.concurrent.atomic.AtomicInteger
/**
* This is a special index that means we're not at an autocompleting state.
*/
private const val NO_STATE_INDEX = 0
class SuggestionsView : FrameLayout, TextWatcher {
private val recyclerView: RecyclerView
// Maps tokens to their respective adapters.
private val adaptersByToken = hashMapOf<String, SuggestionsAdapter<out BaseSuggestionViewHolder>>()
private val externalProvidersByToken = hashMapOf<String, ((query: String) -> Unit)>()
private val localProvidersByToken = hashMapOf<String, HashMap<String, List<SuggestionModel>>>()
private var editor: WeakReference<EditText>? = null
private var completionStartIndex = AtomicInteger(NO_STATE_INDEX)
companion object {
private val SLIDE_TRANSITION = Slide(Gravity.BOTTOM).setDuration(200)
}
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr, 0) {
recyclerView = RecyclerView(context)
val layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL,
false)
recyclerView.itemAnimator = DefaultItemAnimator()
recyclerView.addItemDecoration(TopItemDecoration(context, R.drawable.suggestions_menu_decorator))
recyclerView.layoutManager = layoutManager
recyclerView.visibility = View.GONE
addView(recyclerView)
}
override fun afterTextChanged(s: Editable) {
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
// If we have a deletion.
if (after == 0) {
val deleted = s.subSequence(start, start + count).toString()
if (adaptersByToken.containsKey(deleted) && completionStartIndex.get() > NO_STATE_INDEX) {
// We have removed the '@', '#' or any other action token so halt completion.
cancelSuggestions(true)
}
}
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
// If we don't have any adapter bound to any token bail out.
if (adaptersByToken.isEmpty()) return
val new = s.subSequence(start, start + count).toString()
if (adaptersByToken.containsKey(new)) {
swapAdapter(getAdapterForToken(new)!!)
completionStartIndex.compareAndSet(NO_STATE_INDEX, start + 1)
editor?.let {
// Disable keyboard suggestions when autocompleting.
val editText = it.get()
if (editText != null) {
editText.inputType = editText.inputType or InputType.TYPE_TEXT_VARIATION_FILTER
expand()
}
}
}
if (new.startsWith(" ")) {
// just halts the completion execution
cancelSuggestions(false)
return
}
val prefixEndIndex = editor?.get()?.selectionStart ?: NO_STATE_INDEX
if (prefixEndIndex == NO_STATE_INDEX || prefixEndIndex < completionStartIndex.get()) return
val prefix = s.subSequence(completionStartIndex.get(), editor?.get()?.selectionStart ?: completionStartIndex.get()).toString()
recyclerView.adapter?.let {
it as SuggestionsAdapter
// we need to look up only after the '@'
it.autocomplete(prefix)
val cacheMap = localProvidersByToken[it.token]
if (cacheMap != null && cacheMap[prefix] != null) {
it.addItems(cacheMap[prefix]!!)
} else {
// fetch more suggestions from an external source if any
externalProvidersByToken[it.token]?.invoke(prefix)
}
}
}
private fun swapAdapter(adapter: SuggestionsAdapter<*>): SuggestionsView {
recyclerView.adapter = adapter
// Don't override if user set an item click listener already/
if (!adapter.hasItemClickListener()) {
setOnItemClickListener(adapter) {
// set default item click behavior
}
}
return this
}
fun getAdapterForToken(token: String): SuggestionsAdapter<*>? = adaptersByToken.get(token)
fun anchor(editText: EditText): SuggestionsView {
editText.removeTextChangedListener(this)
editText.addTextChangedListener(this)
editor = WeakReference(editText)
return this
}
fun bindTokenAdapter(adapter: SuggestionsAdapter<*>): SuggestionsView {
adaptersByToken.getOrPut(adapter.token, { adapter })
return this
}
fun addItems(token: String, list: List<SuggestionModel>): SuggestionsView {
if (list.isNotEmpty()) {
val adapter = adapter(token)
localProvidersByToken.getOrPut(token, { hashMapOf() })
.put(adapter.prefix(), list)
if (completionStartIndex.get() > NO_STATE_INDEX && adapter.itemCount == 0) expand()
adapter.addItems(list)
}
return this
}
fun setOnItemClickListener(tokenAdapter: SuggestionsAdapter<*>,
clickListener: (item: SuggestionModel) -> Unit): SuggestionsView {
tokenAdapter.setOnClickListener(object : SuggestionsAdapter.ItemClickListener {
override fun onClick(item: SuggestionModel) {
insertSuggestionOnEditor(item)
clickListener.invoke(item)
cancelSuggestions(true)
collapse()
}
})
return this
}
fun addSuggestionProviderAction(token: String, provider: (query: String) -> Unit): SuggestionsView {
externalProvidersByToken.getOrPut(token, { provider })
return this
}
private fun adapter(token: String): SuggestionsAdapter<*> {
return adaptersByToken[token] ?: throw IllegalStateException("no adapter binds to token \"$token\"")
}
private fun cancelSuggestions(haltCompletion: Boolean) {
// Reset completion start index only if we've deleted the token that triggered completion or
// we finished the completion process.
if (haltCompletion) {
completionStartIndex.set(NO_STATE_INDEX)
}
collapse()
// Re-enable keyboard suggestions.
val editText = editor?.get()
if (editText != null) {
editText.inputType = editText.inputType and InputType.TYPE_TEXT_VARIATION_FILTER.inv()
}
}
private fun insertSuggestionOnEditor(item: SuggestionModel) {
editor?.get()?.let {
val suggestionText = item.text
it.text.replace(completionStartIndex.get(), it.selectionStart, "$suggestionText ")
}
}
private fun collapse() {
TransitionManager.beginDelayedTransition(this, SLIDE_TRANSITION)
recyclerView.visibility = View.GONE
}
private fun expand() {
TransitionManager.beginDelayedTransition(this, SLIDE_TRANSITION)
recyclerView.visibility = View.VISIBLE
}
private class TopItemDecoration() : RecyclerView.ItemDecoration() {
private lateinit var divider: Drawable
private val padding = Rect()
// Custom divider will be used.
constructor(context: Context, @DrawableRes drawableResId: Int) : this() {
val customDrawable = ContextCompat.getDrawable(context, drawableResId)
if (customDrawable != null) {
divider = customDrawable
}
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val left = parent.paddingLeft
val right = (parent.width - parent.paddingRight)
val parentParams = parent.layoutParams as FrameLayout.LayoutParams
val top = parent.top - parentParams.topMargin - parent.paddingTop
val bottom = top + divider.intrinsicHeight
divider.setBounds(left, top, right, bottom)
divider.draw(c)
}
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="800"
android:duration="500"
android:fromYDelta="0.0%p"
android:toYDelta="0.0%p" />
</set>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="350"
android:duration="250"
android:fromYDelta="100.0%"
android:toYDelta="0.0%" />
</set>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#10000000" />
<corners android:radius="5dp" />
<size android:height="2dp" />
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="10dp"
android:height="10dp"
android:viewportWidth="10.0"
android:viewportHeight="10.0">
<path
android:pathData="M5,5m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
android:fillType="evenOdd"
android:fillColor="#FFFFFF"
android:strokeWidth="1"/>
</vector>
\ No newline at end of file
......@@ -9,7 +9,6 @@
tools:context=".authentication.login.ui.LoginFragment">
<android.support.constraint.ConstraintLayout
android:id="@+id/middle_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
......
......@@ -14,19 +14,11 @@
android:layout_centerHorizontal="true"
android:text="@string/title_sign_in_your_server" />
<TextView
android:id="@+id/text_server_protocol"
style="@style/Authentication.TextView"
android:layout_below="@id/text_headline"
android:layout_marginTop="32dp"
android:gravity="center_vertical"
android:text="@string/default_protocol" />
<EditText
android:id="@+id/text_server_url"
style="@style/Authentication.EditText"
android:layout_below="@id/text_headline"
android:layout_marginStart="0dp"
android:layout_marginStart="-4dp"
android:layout_marginTop="32dp"
android:layout_toEndOf="@id/text_server_protocol"
android:cursorVisible="false"
......@@ -35,7 +27,15 @@
android:digits="0123456789abcdefghijklmnopqrstuvwxyz.-/:"
android:inputType="textUri"
android:paddingEnd="0dp"
android:paddingStart="0dp" />
android:paddingStart="2dp" />
<TextView
android:id="@+id/text_server_protocol"
style="@style/Authentication.TextView"
android:layout_below="@id/text_headline"
android:layout_marginTop="32dp"
android:gravity="center_vertical"
android:text="@string/default_protocol" />
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
......
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/constraint_layout"
android:id="@+id/relative_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusableInTouchMode="true"
tools:context=".authentication.signup.ui.SignupFragment">
<ScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/text_headline"
style="@style/Authentication.Headline.TextView"
android:text="@string/title_sign_up"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
android:layout_gravity="center"
android:text="@string/title_sign_up" />
<EditText
android:id="@+id/text_name"
......@@ -22,10 +31,7 @@
android:drawableStart="@drawable/ic_person_black_24dp"
android:hint="@string/msg_name"
android:imeOptions="actionNext"
android:inputType="textCapWords"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_headline" />
android:inputType="textCapWords" />
<EditText
android:id="@+id/text_username"
......@@ -34,10 +40,7 @@
android:drawableStart="@drawable/ic_at_black_24dp"
android:hint="@string/msg_username"
android:imeOptions="actionNext"
android:inputType="text"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_name" />
android:inputType="text" />
<EditText
android:id="@+id/text_password"
......@@ -46,53 +49,51 @@
android:drawableStart="@drawable/ic_lock_black_24dp"
android:hint="@string/msg_password"
android:imeOptions="actionNext"
android:inputType="textPassword"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_username" />
android:inputType="textPassword" />
<EditText
android:id="@+id/text_email"
style="@style/Authentication.EditText"
android:layout_marginBottom="16dp"
android:layout_marginTop="16dp"
android:drawableStart="@drawable/ic_email_black_24dp"
android:hint="@string/msg_email"
android:imeOptions="actionDone"
android:inputType="textEmailAddress"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_password" />
android:inputType="textEmailAddress" />
</LinearLayout>
</ScrollView>
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_below="@id/scroll_view"
android:layout_centerHorizontal="true"
android:layout_marginTop="16dp"
android:visibility="gone"
app:indicatorName="BallPulseIndicator"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_email"
tools:visibility="visible" />
<LinearLayout
android:id="@+id/bottom_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:orientation="vertical">
<TextView
android:id="@+id/text_new_user_agreement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins"
android:layout_marginStart="@dimen/screen_edge_left_and_right_margins"
android:layout_gravity="center"
android:layout_margin="@dimen/screen_edge_left_and_right_margins"
android:gravity="center"
android:textColorLink="@color/colorAccent"
app:layout_constraintBottom_toTopOf="@+id/button_sign_up"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
android:textColorLink="@color/colorAccent" />
<Button
android:id="@+id/button_sign_up"
style="@style/Authentication.Button"
android:text="@string/title_sign_up"
app:layout_constraintBottom_toBottomOf="parent" />
android:text="@string/title_sign_up" />
</LinearLayout>
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
</RelativeLayout>
\ No newline at end of file
......@@ -28,8 +28,16 @@
layout="@layout/message_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<chat.rocket.android.widget.autocompletion.ui.SuggestionsView
android:id="@+id/suggestions_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/layout_message_composer"
android:background="@color/suggestion_background_color" />
<include
android:id="@+id/layout_message_composer"
layout="@layout/message_composer"
......@@ -58,15 +66,15 @@
android:id="@+id/connection_status_text"
android:layout_width="match_parent"
android:layout_height="32dp"
android:alpha="0"
android:background="@color/colorPrimary"
android:elevation="4dp"
android:textColor="@color/white"
android:gravity="center"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="@color/white"
android:visibility="gone"
android:alpha="0"
tools:alpha="1"
tools:visibility="visible"
tools:text="connected"/>
tools:text="connected"
tools:visibility="visible" />
</RelativeLayout>
......@@ -27,6 +27,18 @@
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/divider" />
<Button
android:id="@+id/button_join_chat"
android:layout_width="match_parent"
android:layout_height="45dp"
android:background="@color/white"
android:text="@string/action_join_chat"
android:textColor="@color/colorAccent"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/divider" />
<LinearLayout
android:id="@+id/input_container"
android:layout_width="match_parent"
......
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:layout_marginEnd="2dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="2dp"
android:layout_marginStart="4dp"
android:layout_marginTop="2dp"
android:background="@color/suggestion_background_color">
<FrameLayout
android:id="@+id/image_avatar_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_avatar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="4dp"
app:roundedCornerRadius="3dp"
tools:src="@tools:sample/avatars" />
<ImageView
android:id="@+id/image_status"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_gravity="bottom|end"
android:background="@drawable/user_status_white"
android:padding="2dp" />
</FrameLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/image_avatar_container"
android:layout_toRightOf="@id/image_avatar_container"
android:background="@color/suggestion_background_color">
<TextView
android:id="@+id/text_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:maxLines="1"
android:textColor="@color/black"
android:textSize="16sp"
tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/text_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_toEndOf="@+id/text_username"
android:layout_toRightOf="@+id/text_username"
android:maxLines="1"
android:paddingLeft="8dp"
android:paddingStart="8dp"
android:textColor="@color/gray_material"
android:textSize="16sp"
tools:text="@tools:sample/full_names" />
</RelativeLayout>
</RelativeLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="2dp"
android:background="@color/suggestion_background_color">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/image_avatar_container"
android:layout_toRightOf="@id/image_avatar_container"
android:background="@color/suggestion_background_color">
<TextView
android:id="@+id/text_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:maxLines="1"
android:textColor="@color/black"
android:textSize="16sp"
tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/text_fullname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_toEndOf="@+id/text_name"
android:layout_toRightOf="@+id/text_name"
android:maxLines="1"
android:paddingLeft="8dp"
android:paddingStart="8dp"
android:textColor="@color/gray_material"
android:textSize="16sp"
tools:text="@tools:sample/full_names" />
</RelativeLayout>
</RelativeLayout>
\ No newline at end of file
......@@ -24,6 +24,8 @@
<string name="action_logout">Sair</string>
<string name="action_files">Arquivos</string>
<string name="action_confirm_password">Confirme a nova senha</string>
<string name="action_join_chat">Entrar no Chat</string>
<!-- Settings List -->
<string-array name="settings_actions">
......
......@@ -15,7 +15,7 @@
<color name="colorUserStatusOnline">#2FE1A8</color>
<color name="colorUserStatusBusy">#F33E5B</color>
<color name="colorUserStatusAway">#FDD236</color>
<color name="colorUserStatusOffline">#1F2228</color>
<color name="colorUserStatusOffline">#d9d9d9</color>
<color name="colorDrawableTintGrey">#9FA2A8</color>
......@@ -36,4 +36,7 @@
<color name="colorEmojiIcon">#FF767676</color>
<!-- Suggestions -->
<color name="suggestion_background_color">@android:color/white</color>
</resources>
......@@ -32,4 +32,7 @@
<dimen name="padding_mention">4dp</dimen>
<dimen name="radius_mention">6dp</dimen>
<!-- Autocomplete Popup -->
<dimen name="popup_max_height">150dp</dimen>
</resources>
\ No newline at end of file
......@@ -25,6 +25,7 @@
<string name="action_logout">Logout</string>
<string name="action_files">Files</string>
<string name="action_confirm_password">Confirm Password Change</string>
<string name="action_join_chat">Join Chat</string>
<!-- Settings List -->
<string-array name="settings_actions">
......
......@@ -14,7 +14,7 @@ buildscript {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}"
classpath 'com.google.gms:google-services:3.2.0'
classpath 'io.fabric.tools:gradle:1.+'
classpath 'io.fabric.tools:gradle:1.25.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
......
......@@ -4,7 +4,7 @@ ext {
compileSdk : 27,
targetSdk : 27,
buildTools : '27.0.3',
kotlin : '1.2.21',
kotlin : '1.2.30',
coroutine : '0.22',
dokka : '0.9.15',
......@@ -25,8 +25,7 @@ ext {
rxBinding : '2.0.0',
fresco : '1.8.1',
kotshi : '0.3.0',
frescoImageViewer : '0.5.0',
androidSvg : 'master-SNAPSHOT',
frescoImageViewer : '0.5.1',
markwon : '1.0.3',
sheetMenu : '1.3.3',
aVLoadingIndicatorView : '2.1.3',
......@@ -86,8 +85,7 @@ ext {
kotshiApi : "se.ansman.kotshi:api:${versions.kotshi}",
kotshiCompiler : "se.ansman.kotshi:compiler:${versions.kotshi}",
frescoImageViewer : "com.github.stfalcon:frescoimageviewer:${versions.frescoImageViewer}",
androidSvg : "com.github.BigBadaboom:androidsvg:${versions.androidSvg}",
frescoImageViewer : "com.github.luciofm:FrescoImageViewer:${versions.frescoImageViewer}",
markwon : "ru.noties:markwon:${versions.markwon}",
markwonImageLoader : "ru.noties:markwon-image-loader:${versions.markwon}",
......@@ -96,8 +94,6 @@ ext {
aVLoadingIndicatorView : "com.wang.avi:library:${versions.aVLoadingIndicatorView}",
swipeBackLayout : "me.imid.swipebacklayout.lib:library:${versions.swipeBackLayout}",
// For testing
junit : "junit:junit:$versions.junit",
expressoCore : "com.android.support.test.espresso:espresso-core:${versions.expresso}",
......
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