Commit f882e57c authored by pcforgeek's avatar pcforgeek

Merge remote-tracking branch 'upstream/develop-2.x' into add-day-marker-in-chatroom

parents 09302e5f 2e7a0037
...@@ -12,8 +12,8 @@ android { ...@@ -12,8 +12,8 @@ android {
applicationId "chat.rocket.android" applicationId "chat.rocket.android"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion versions.targetSdk targetSdkVersion versions.targetSdk
versionCode 1011 versionCode 2000
versionName "2.0.0-dev9" versionName "2.0.0-alpha1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
} }
...@@ -32,7 +32,6 @@ android { ...@@ -32,7 +32,6 @@ android {
signingConfig signingConfigs.release signingConfig signingConfigs.release
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
applicationIdSuffix ".dev"
} }
debug { debug {
...@@ -95,14 +94,9 @@ dependencies { ...@@ -95,14 +94,9 @@ dependencies {
implementation libraries.kotshiApi implementation libraries.kotshiApi
implementation libraries.frescoImageViewer implementation libraries.frescoImageViewer
implementation (libraries.androidSvg) {
exclude group: 'org.jetbrains', module: 'annotations-java5'
}
implementation libraries.markwon implementation libraries.markwon
implementation (libraries.markwonImageLoader) { implementation libraries.markwonImageLoader
exclude group: 'com.caverock', module: 'androidsvg'
}
implementation libraries.sheetMenu implementation libraries.sheetMenu
......
...@@ -5,6 +5,7 @@ import android.support.v4.graphics.drawable.DrawableCompat ...@@ -5,6 +5,7 @@ import android.support.v4.graphics.drawable.DrawableCompat
import android.widget.EditText import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.common.model.UserStatus
object DrawableHelper { object DrawableHelper {
...@@ -78,7 +79,7 @@ object DrawableHelper { ...@@ -78,7 +79,7 @@ object DrawableHelper {
* @param drawables The array of Drawable. * @param drawables The array of Drawable.
* @see compoundDrawable * @see compoundDrawable
*/ */
fun compoundDrawables(textView: Array<EditText>, drawables: Array<Drawable>) { fun compoundDrawables(textView: Array<TextView>, drawables: Array<Drawable>) {
if (textView.size != drawables.size) { if (textView.size != drawables.size) {
return return
} else { } else {
...@@ -104,15 +105,15 @@ object DrawableHelper { ...@@ -104,15 +105,15 @@ object DrawableHelper {
* @param context The context. * @param context The context.
* @return The user status drawable. * @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() val userStatusDrawable = getDrawableFromId(R.drawable.ic_user_status_black, context).mutate()
wrapDrawable(userStatusDrawable) wrapDrawable(userStatusDrawable)
when (userStatus) { when (userStatus) {
// TODO: create a enum or check if it will come from the SDK is UserStatus.Online -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusOnline)
"online" -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusOnline) is UserStatus.Busy -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusBusy)
"busy" -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusBusy) is UserStatus.Away -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusAway)
"away" -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusAway) is UserStatus.Offline -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusOffline)
"offline" -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusOffline) else -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusOffline)
} }
return userStatusDrawable 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 ...@@ -19,12 +19,15 @@ import javax.inject.Inject
class SignupFragment : Fragment(), SignupView { class SignupFragment : Fragment(), SignupView {
@Inject lateinit var presenter: SignupPresenter @Inject lateinit var presenter: SignupPresenter
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener { private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
if (KeyboardHelper.isSoftKeyboardShown(constraint_layout.rootView)) { if (KeyboardHelper.isSoftKeyboardShown(relative_layout.rootView)) {
text_new_user_agreement.setVisible(false) bottom_container.setVisible(false)
} else { } else {
text_new_user_agreement.setVisible(true) bottom_container.apply {
postDelayed({
setVisible(true)
}, 3)
}
} }
} }
...@@ -42,24 +45,22 @@ class SignupFragment : Fragment(), SignupView { ...@@ -42,24 +45,22 @@ class SignupFragment : Fragment(), SignupView {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
activity?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
tintEditTextDrawableStart() tintEditTextDrawableStart()
} }
constraint_layout.viewTreeObserver.addOnGlobalLayoutListener(layoutListener) relative_layout.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
setUpNewUserAgreementListener() setUpNewUserAgreementListener()
button_sign_up.setOnClickListener { 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() { override fun onDestroyView() {
relative_layout.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
super.onDestroyView() super.onDestroyView()
constraint_layout.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
} }
override fun alertBlankName() { override fun alertBlankName() {
...@@ -146,7 +147,7 @@ class SignupFragment : Fragment(), SignupView { ...@@ -146,7 +147,7 @@ class SignupFragment : Fragment(), SignupView {
private fun enableUserInput(value: Boolean) { private fun enableUserInput(value: Boolean) {
button_sign_up.isEnabled = value button_sign_up.isEnabled = value
text_name.isEnabled = value text_username.isEnabled = value
text_username.isEnabled = value text_username.isEnabled = value
text_password.isEnabled = value text_password.isEnabled = value
text_email.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( ...@@ -120,7 +120,6 @@ class ChatRoomAdapter(
val indexOfFirst = dataSet.indexOfFirst { it.messageId == message.messageId } val indexOfFirst = dataSet.indexOfFirst { it.messageId == message.messageId }
Timber.d("index: $index") Timber.d("index: $index")
if (index > -1) { if (index > -1) {
message.nextDownStreamMessage = dataSet[index].nextDownStreamMessage
dataSet[index] = message dataSet[index] = message
notifyItemChanged(index) notifyItemChanged(index)
while (dataSet[index].nextDownStreamMessage != null) { while (dataSet[index].nextDownStreamMessage != null) {
......
package chat.rocket.android.chatroom.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.CommandSuggestionsAdapter.CommandSuggestionsViewHolder
import chat.rocket.android.chatroom.viewmodel.suggestion.CommandSuggestionViewModel
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.ui.BaseSuggestionViewHolder
import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter
class CommandSuggestionsAdapter : SuggestionsAdapter<CommandSuggestionsViewHolder>(token = "/",
constraint = CONSTRAINT_BOUND_TO_START, threshold = UNLIMITED_RESULT_COUNT) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommandSuggestionsViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.suggestion_command_item, parent,
false)
return CommandSuggestionsViewHolder(view)
}
class CommandSuggestionsViewHolder(view: View) : BaseSuggestionViewHolder(view) {
override fun bind(item: SuggestionModel, itemClickListener: SuggestionsAdapter.ItemClickListener?) {
item as CommandSuggestionViewModel
with(itemView) {
val nameTextView = itemView.findViewById<TextView>(R.id.text_command_name)
val descriptionTextView = itemView.findViewById<TextView>(R.id.text_command_description)
nameTextView.text = "/${item.text}"
val res = context.resources
val id = res.getIdentifier(item.description, "string", context.packageName)
val description = if (id > 0) res.getString(id) else ""
descriptionTextView.text = description.toLowerCase()
setOnClickListener {
itemClickListener?.onClick(item)
}
}
}
}
}
\ No newline at end of file
...@@ -2,6 +2,8 @@ package chat.rocket.android.chatroom.adapter ...@@ -2,6 +2,8 @@ package chat.rocket.android.chatroom.adapter
import android.view.View import android.view.View
import chat.rocket.android.chatroom.viewmodel.ImageAttachmentViewModel 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 chat.rocket.android.widget.emoji.EmojiReactionListener
import com.stfalcon.frescoimageviewer.ImageViewer import com.stfalcon.frescoimageviewer.ImageViewer
import kotlinx.android.synthetic.main.message_attachment.view.* import kotlinx.android.synthetic.main.message_attachment.view.*
...@@ -20,12 +22,21 @@ class ImageAttachmentViewHolder(itemView: View, ...@@ -20,12 +22,21 @@ class ImageAttachmentViewHolder(itemView: View,
override fun bindViews(data: ImageAttachmentViewModel) { override fun bindViews(data: ImageAttachmentViewModel) {
with(itemView) { 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 file_name.text = data.attachmentTitle
image_attachment.setOnClickListener { view -> image_attachment.setOnClickListener { view ->
// TODO - implement a proper image viewer with a proper Transition // TODO - implement a proper image viewer with a proper Transition
val builder = ImageViewer.createPipelineDraweeControllerBuilder()
.setAutoPlayAnimations(true)
ImageViewer.Builder(view.context, listOf(data.attachmentUrl)) ImageViewer.Builder(view.context, listOf(data.attachmentUrl))
.setStartPosition(0) .setStartPosition(0)
.hideStatusBar(false)
.setCustomDraweeControllerBuilder(builder)
.show() .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.suggestion.PeopleSuggestionViewModel
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.ui.BaseSuggestionViewHolder
import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter
import chat.rocket.common.model.UserStatus
import com.facebook.drawee.view.SimpleDraweeView
class PeopleSuggestionsAdapter : SuggestionsAdapter<PeopleSuggestionViewHolder>("@") {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PeopleSuggestionViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.suggestion_member_item, parent,
false)
return PeopleSuggestionViewHolder(view)
}
class PeopleSuggestionViewHolder(view: View) : BaseSuggestionViewHolder(view) {
override fun bind(item: SuggestionModel, itemClickListener: SuggestionsAdapter.ItemClickListener?) {
item as PeopleSuggestionViewModel
with(itemView) {
val username = itemView.findViewById<TextView>(R.id.text_username)
val name = itemView.findViewById<TextView>(R.id.text_name)
val avatar = itemView.findViewById<SimpleDraweeView>(R.id.image_avatar)
val statusView = itemView.findViewById<ImageView>(R.id.image_status)
username.text = item.username
name.text = item.name
if (item.imageUri.isEmpty()) {
avatar.setVisible(false)
} else {
avatar.setVisible(true)
avatar.setImageURI(item.imageUri)
}
val status = item.status ?: UserStatus.Offline()
val statusDrawable = DrawableHelper.getUserStatusDrawable(status, itemView.context)
statusView.setImageDrawable(statusDrawable)
setOnClickListener {
itemClickListener?.onClick(item)
}
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.RoomSuggestionsAdapter.RoomSuggestionsViewHolder
import chat.rocket.android.chatroom.viewmodel.suggestion.ChatRoomSuggestionViewModel
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.ui.BaseSuggestionViewHolder
import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter
class RoomSuggestionsAdapter : SuggestionsAdapter<RoomSuggestionsViewHolder>("#") {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RoomSuggestionsViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.suggestion_room_item, parent,
false)
return RoomSuggestionsViewHolder(view)
}
class RoomSuggestionsViewHolder(view: View) : BaseSuggestionViewHolder(view) {
override fun bind(item: SuggestionModel, itemClickListener: SuggestionsAdapter.ItemClickListener?) {
item as ChatRoomSuggestionViewModel
with(itemView) {
val fullname = itemView.findViewById<TextView>(R.id.text_fullname)
val name = itemView.findViewById<TextView>(R.id.text_name)
name.text = item.name
fullname.text = item.fullName
setOnClickListener {
itemClickListener?.onClick(item)
}
}
}
}
}
\ No newline at end of file
...@@ -2,6 +2,9 @@ package chat.rocket.android.chatroom.presentation ...@@ -2,6 +2,9 @@ package chat.rocket.android.chatroom.presentation
import android.net.Uri import android.net.Uri
import chat.rocket.android.chatroom.viewmodel.BaseViewModel import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.chatroom.viewmodel.suggestion.ChatRoomSuggestionViewModel
import chat.rocket.android.chatroom.viewmodel.suggestion.CommandSuggestionViewModel
import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewModel
import chat.rocket.android.core.behaviours.LoadingView import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.core.internal.realtime.State import chat.rocket.core.internal.realtime.State
...@@ -100,5 +103,19 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -100,5 +103,19 @@ interface ChatRoomView : LoadingView, MessageView {
fun showInvalidFileSize(fileSize: Int, maxFileSize: Int) fun showInvalidFileSize(fileSize: Int, maxFileSize: Int)
fun showConnectionState(state: State) fun showConnectionState(state: State)
fun populatePeopleSuggestions(members: List<PeopleSuggestionViewModel>)
fun populateRoomSuggestions(chatRooms: List<ChatRoomSuggestionViewModel>)
/**
* This user has joined the chat callback.
*/
fun onJoined()
fun showReactionsPopup(messageId: String) fun showReactionsPopup(messageId: String)
/**
* Show list of commands.
*
* @param commands The list of available commands.
*/
fun populateCommandSuggestions(commands: List<CommandSuggestionViewModel>)
} }
\ No newline at end of file
...@@ -23,13 +23,19 @@ import javax.inject.Inject ...@@ -23,13 +23,19 @@ import javax.inject.Inject
import timber.log.Timber 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 { return Intent(this, ChatRoomActivity::class.java).apply {
putExtra(INTENT_CHAT_ROOM_ID, chatRoomId) putExtra(INTENT_CHAT_ROOM_ID, chatRoomId)
putExtra(INTENT_CHAT_ROOM_NAME, chatRoomName) putExtra(INTENT_CHAT_ROOM_NAME, chatRoomName)
putExtra(INTENT_CHAT_ROOM_TYPE, chatRoomType) putExtra(INTENT_CHAT_ROOM_TYPE, chatRoomType)
putExtra(INTENT_IS_CHAT_ROOM_READ_ONLY, isChatRoomReadOnly) putExtra(INTENT_IS_CHAT_ROOM_READ_ONLY, isChatRoomReadOnly)
putExtra(INTENT_CHAT_ROOM_LAST_SEEN, chatRoomLastSeen) 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" ...@@ -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_CHAT_ROOM_TYPE = "chat_room_type"
private const val INTENT_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only" private const val INTENT_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only"
private const val INTENT_CHAT_ROOM_LAST_SEEN = "chat_room_last_seen" 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 { class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment> @Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
...@@ -50,6 +57,7 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -50,6 +57,7 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
private lateinit var chatRoomName: String private lateinit var chatRoomName: String
private lateinit var chatRoomType: String private lateinit var chatRoomType: String
private var isChatRoomReadOnly: Boolean = false private var isChatRoomReadOnly: Boolean = false
private var isChatRoomSubscribed: Boolean = true
private var chatRoomLastSeen: Long = -1L private var chatRoomLastSeen: Long = -1L
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
...@@ -76,8 +84,11 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -76,8 +84,11 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
chatRoomLastSeen = intent.getLongExtra(INTENT_CHAT_ROOM_LAST_SEEN, -1) chatRoomLastSeen = intent.getLongExtra(INTENT_CHAT_ROOM_LAST_SEEN, -1)
isChatRoomSubscribed = intent.getBooleanExtra(INTENT_CHAT_IS_SUBSCRIBED, true)
addFragment("ChatRoomFragment", R.id.fragment_container) { addFragment("ChatRoomFragment", R.id.fragment_container) {
newInstance(chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly, chatRoomLastSeen) newInstance(chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly, chatRoomLastSeen,
isChatRoomSubscribed)
} }
} }
......
...@@ -15,11 +15,14 @@ import android.support.v7.widget.LinearLayoutManager ...@@ -15,11 +15,14 @@ import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.view.* import android.view.*
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.ChatRoomAdapter import chat.rocket.android.chatroom.adapter.*
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.presentation.ChatRoomView import chat.rocket.android.chatroom.presentation.ChatRoomView
import chat.rocket.android.chatroom.viewmodel.BaseViewModel import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.chatroom.viewmodel.MessageViewModel import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.chatroom.viewmodel.suggestion.ChatRoomSuggestionViewModel
import chat.rocket.android.chatroom.viewmodel.suggestion.CommandSuggestionViewModel
import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewModel
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.helper.KeyboardHelper import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.MessageParser import chat.rocket.android.helper.MessageParser
...@@ -37,7 +40,12 @@ import kotlinx.android.synthetic.main.message_list.* ...@@ -37,7 +40,12 @@ import kotlinx.android.synthetic.main.message_list.*
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject 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 { return ChatRoomFragment().apply {
arguments = Bundle(1).apply { arguments = Bundle(1).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId) putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
...@@ -45,6 +53,7 @@ fun newInstance(chatRoomId: String, chatRoomName: String, chatRoomType: String, ...@@ -45,6 +53,7 @@ fun newInstance(chatRoomId: String, chatRoomName: String, chatRoomType: String,
putString(BUNDLE_CHAT_ROOM_TYPE, chatRoomType) putString(BUNDLE_CHAT_ROOM_TYPE, chatRoomType)
putBoolean(BUNDLE_IS_CHAT_ROOM_READ_ONLY, isChatRoomReadOnly) putBoolean(BUNDLE_IS_CHAT_ROOM_READ_ONLY, isChatRoomReadOnly)
putLong(BUNDLE_CHAT_ROOM_LAST_SEEN, chatRoomLastSeen) putLong(BUNDLE_CHAT_ROOM_LAST_SEEN, chatRoomLastSeen)
putBoolean(BUNDLE_CHAT_ROOM_IS_SUBSCRIBED, isSubscribed)
} }
} }
} }
...@@ -55,17 +64,18 @@ private const val BUNDLE_CHAT_ROOM_TYPE = "chat_room_type" ...@@ -55,17 +64,18 @@ private const val BUNDLE_CHAT_ROOM_TYPE = "chat_room_type"
private const val BUNDLE_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only" private const val BUNDLE_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only"
private const val REQUEST_CODE_FOR_PERFORM_SAF = 42 private const val REQUEST_CODE_FOR_PERFORM_SAF = 42
private const val BUNDLE_CHAT_ROOM_LAST_SEEN = "chat_room_last_seen" 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 { class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiReactionListener {
@Inject lateinit var presenter: ChatRoomPresenter @Inject lateinit var presenter: ChatRoomPresenter
@Inject lateinit var parser: MessageParser @Inject lateinit var parser: MessageParser
private lateinit var adapter: ChatRoomAdapter private lateinit var adapter: ChatRoomAdapter
private lateinit var chatRoomId: String private lateinit var chatRoomId: String
private lateinit var chatRoomName: String private lateinit var chatRoomName: String
private lateinit var chatRoomType: String private lateinit var chatRoomType: String
private lateinit var emojiKeyboardPopup: EmojiKeyboardPopup private var isSubscribed: Boolean = true
private var isChatRoomReadOnly: Boolean = false private var isChatRoomReadOnly: Boolean = false
private lateinit var emojiKeyboardPopup: EmojiKeyboardPopup
private var chatRoomLastSeen: Long = -1 private var chatRoomLastSeen: Long = -1
private lateinit var actionSnackbar: ActionSnackbar private lateinit var actionSnackbar: ActionSnackbar
private var citation: String? = null private var citation: String? = null
...@@ -91,6 +101,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -91,6 +101,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
chatRoomName = bundle.getString(BUNDLE_CHAT_ROOM_NAME) chatRoomName = bundle.getString(BUNDLE_CHAT_ROOM_NAME)
chatRoomType = bundle.getString(BUNDLE_CHAT_ROOM_TYPE) chatRoomType = bundle.getString(BUNDLE_CHAT_ROOM_TYPE)
isChatRoomReadOnly = bundle.getBoolean(BUNDLE_IS_CHAT_ROOM_READ_ONLY) 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) chatRoomLastSeen = bundle.getLong(BUNDLE_CHAT_ROOM_LAST_SEEN)
} else { } else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" } requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
...@@ -105,10 +116,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -105,10 +116,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
setupToolbar(chatRoomName) setupToolbar(chatRoomName)
presenter.loadMessages(chatRoomId, chatRoomType) presenter.loadMessages(chatRoomId, chatRoomType)
presenter.loadChatRooms()
setupRecyclerView() setupRecyclerView()
setupFab() setupFab()
setupMessageComposer() setupMessageComposer()
setupSuggestionsView()
setupActionSnackbar() setupActionSnackbar()
} }
...@@ -174,11 +186,12 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -174,11 +186,12 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
// track the message sent immediately after the current message // 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 // Loop over received messages to determine first unread
for (i in dataSet.indices) { for (i in dataSet.indices) {
val msgModel = dataSet[i] val msgModel = dataSet[i]
if (msgModel is MessageViewModel) { if (msgModel is MessageViewModel) {
val msg = msgModel.rawData val msg = msgModel.rawData
if (msg.timestamp < chatRoomLastSeen) { if (msg.timestamp < chatRoomLastSeen) {
...@@ -217,12 +230,17 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -217,12 +230,17 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
if (oldMessagesCount == 0 && dataSet.isNotEmpty()) { if (oldMessagesCount == 0 && dataSet.isNotEmpty()) {
recycler_view.scrollToPosition(0) recycler_view.scrollToPosition(0)
} }
presenter.loadActiveMembers(chatRoomId, chatRoomType, filterSelfOut = true)
} }
} }
override fun sendMessage(text: String) { override fun sendMessage(text: String) {
if (!text.isBlank()) { if (!text.isBlank()) {
presenter.sendMessage(chatRoomId, text, editingMessageId) if (!text.startsWith("/")) {
presenter.sendMessage(chatRoomId, text, editingMessageId)
} else {
presenter.runCommand(text, chatRoomId)
}
} }
} }
...@@ -291,6 +309,18 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -291,6 +309,18 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error)) override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun populatePeopleSuggestions(members: List<PeopleSuggestionViewModel>) {
suggestions_view.addItems("@", members)
}
override fun populateRoomSuggestions(chatRooms: List<ChatRoomSuggestionViewModel>) {
suggestions_view.addItems("#", chatRooms)
}
override fun populateCommandSuggestions(commands: List<CommandSuggestionViewModel>) {
suggestions_view.addItems("/", commands)
}
override fun copyToClipboard(message: String) { override fun copyToClipboard(message: String) {
activity?.apply { activity?.apply {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
...@@ -380,6 +410,13 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -380,6 +410,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 = { private val dismissStatus = {
connection_status_text.fadeOut() connection_status_text.fadeOut()
} }
...@@ -410,6 +447,10 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -410,6 +447,10 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
if (isChatRoomReadOnly) { if (isChatRoomReadOnly) {
text_room_is_read_only.setVisible(true) text_room_is_read_only.setVisible(true)
input_container.setVisible(false) input_container.setVisible(false)
} else if (!isSubscribed) {
input_container.setVisible(false)
button_join_chat.setVisible(true)
button_join_chat.setOnClickListener { presenter.joinChat(chatRoomId) }
} else { } else {
button_send.alpha = 0f button_send.alpha = 0f
button_send.setVisible(false) button_send.setVisible(false)
...@@ -476,14 +517,37 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -476,14 +517,37 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
} }
private fun setupSuggestionsView() {
suggestions_view.anchorTo(text_message)
.setMaximumHeight(resources.getDimensionPixelSize(R.dimen.suggestions_box_max_height))
.addTokenAdapter(PeopleSuggestionsAdapter())
.addTokenAdapter(CommandSuggestionsAdapter())
.addTokenAdapter(RoomSuggestionsAdapter())
.addSuggestionProviderAction("@") { query ->
if (query.isNotEmpty()) {
presenter.spotlight(query, PEOPLE, true)
}
}
.addSuggestionProviderAction("#") { query ->
if (query.isNotEmpty()) {
presenter.loadChatRooms()
}
}
.addSuggestionProviderAction("/") { _ ->
presenter.loadCommands()
}
presenter.loadCommands()
}
private fun openEmojiKeyboardPopup() { private fun openEmojiKeyboardPopup() {
if (!emojiKeyboardPopup.isShowing()) { if (!emojiKeyboardPopup.isShowing) {
// If keyboard is visible, simply show the popup // If keyboard is visible, simply show the popup
if (emojiKeyboardPopup.isKeyboardOpen) { if (emojiKeyboardPopup.isKeyboardOpen) {
emojiKeyboardPopup.showAtBottom() emojiKeyboardPopup.showAtBottom()
} else { } else {
// Open the text keyboard first and immediately after that show the emoji popup // Open the text keyboard first and immediately after that show the emoji popup
text_message.setFocusableInTouchMode(true) text_message.isFocusableInTouchMode = true
text_message.requestFocus() text_message.requestFocus()
emojiKeyboardPopup.showAtBottomPending() emojiKeyboardPopup.showAtBottomPending()
KeyboardHelper.showSoftKeyboard(text_message) KeyboardHelper.showSoftKeyboard(text_message)
......
...@@ -96,31 +96,53 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -96,31 +96,53 @@ class ViewModelMapper @Inject constructor(private val context: Context,
} }
private fun mapFileAttachment(message: Message, attachment: FileAttachment): BaseViewModel<*>? { private fun mapFileAttachment(message: Message, attachment: FileAttachment): BaseViewModel<*>? {
val attachmentUrl = attachmentUrl("$baseUrl${attachment.url}") val attachmentUrl = attachmentUrl(attachment)
val attachmentTitle = attachment.title val attachmentTitle = attachmentTitle(attachment)
val id = "${message.id}_${attachment.titleLink}".hashCode().toLong() val id = attachmentId(message, attachment)
return when (attachment) { return when (attachment) {
is ImageAttachment -> ImageAttachmentViewModel(message, attachment, message.id, is ImageAttachment -> ImageAttachmentViewModel(message, attachment, message.id,
attachmentUrl, attachmentTitle ?: "", id, getReactions(message)) attachmentUrl, attachmentTitle, id, getReactions(message))
is VideoAttachment -> VideoAttachmentViewModel(message, attachment, message.id, is VideoAttachment -> VideoAttachmentViewModel(message, attachment, message.id,
attachmentUrl, attachmentTitle ?: "", id, getReactions(message)) attachmentUrl, attachmentTitle, id, getReactions(message))
is AudioAttachment -> AudioAttachmentViewModel(message, attachment, message.id, is AudioAttachment -> AudioAttachmentViewModel(message, attachment, message.id,
attachmentUrl, attachmentTitle ?: "", id, getReactions(message)) attachmentUrl, attachmentTitle, id, getReactions(message))
else -> null else -> null
} }
} }
private fun attachmentUrl(url: String): String { private fun attachmentId(message: Message, attachment: FileAttachment): Long {
var response = url return "${message.id}_${attachment.url}".hashCode().toLong()
val httpUrl = HttpUrl.parse(url) }
httpUrl?.let {
response = it.newBuilder().apply { private fun attachmentTitle(attachment: FileAttachment): CharSequence {
addQueryParameter("rc_uid", token?.userId) return with(attachment) {
addQueryParameter("rc_token", token?.authToken) title?.let { return@with it }
}.build().toString()
val fileUrl = HttpUrl.parse(url)
fileUrl?.let {
return@with it.pathSegments().last()
}
return@with ""
} }
}
private fun attachmentUrl(attachment: FileAttachment): String {
return with(attachment) {
if (url.startsWith("http")) return@with url
val fullUrl = "$baseUrl$url"
val httpUrl = HttpUrl.parse(fullUrl)
httpUrl?.let {
return@with it.newBuilder().apply {
addQueryParameter("rc_uid", token?.userId)
addQueryParameter("rc_token", token?.authToken)
}.build().toString()
}
return response // Fallback to baseUrl + url
return@with fullUrl
}
} }
private suspend fun mapMessage(message: Message): MessageViewModel = withContext(CommonPool) { private suspend fun mapMessage(message: Message): MessageViewModel = withContext(CommonPool) {
......
package chat.rocket.android.chatroom.viewmodel.suggestion
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
class ChatRoomSuggestionViewModel(text: String,
val fullName: String,
val name: String,
searchList: List<String>) : SuggestionModel(text, searchList, false) {
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel.suggestion
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
class CommandSuggestionViewModel(text: String,
val description: String,
searchList: List<String>) : SuggestionModel(text, searchList)
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel.suggestion
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.common.model.UserStatus
class PeopleSuggestionViewModel(val imageUri: String,
text: String,
val username: String,
val name: String,
val status: UserStatus?,
pinned: Boolean = false,
searchList: List<String>) : SuggestionModel(text, searchList, pinned) {
override fun toString(): String {
return "PeopleSuggestionViewModel(imageUri='$imageUri', username='$username', name='$name', status=$status, pinned=$pinned)"
}
}
\ No newline at end of file
...@@ -9,12 +9,12 @@ import chat.rocket.android.server.infraestructure.chatRooms ...@@ -9,12 +9,12 @@ import chat.rocket.android.server.infraestructure.chatRooms
import chat.rocket.android.server.infraestructure.state import chat.rocket.android.server.infraestructure.state
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.model.BaseRoom import chat.rocket.common.model.*
import chat.rocket.common.model.RoomType
import chat.rocket.core.internal.model.Subscription import chat.rocket.core.internal.model.Subscription
import chat.rocket.core.internal.realtime.State import chat.rocket.core.internal.realtime.State
import chat.rocket.core.internal.realtime.StreamMessage import chat.rocket.core.internal.realtime.StreamMessage
import chat.rocket.core.internal.realtime.Type 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.ChatRoom
import chat.rocket.core.model.Room import chat.rocket.core.model.Room
import kotlinx.coroutines.experimental.* import kotlinx.coroutines.experimental.*
...@@ -34,6 +34,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -34,6 +34,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
factory: ConnectionManagerFactory) { factory: ConnectionManagerFactory) {
private val manager: ConnectionManager = factory.create(serverInteractor.get()!!) private val manager: ConnectionManager = factory.create(serverInteractor.get()!!)
private val currentServer = serverInteractor.get()!! private val currentServer = serverInteractor.get()!!
private val client = manager.client
private var reloadJob: Deferred<List<ChatRoom>>? = null private var reloadJob: Deferred<List<ChatRoom>>? = null
private val settings = settingsRepository.get(currentServer)!! private val settings = settingsRepository.get(currentServer)!!
...@@ -69,7 +70,9 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -69,7 +70,9 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
} }
navigator.toChatRoom(chatRoom.id, roomName, 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,8 +82,41 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -79,8 +82,41 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
fun chatRoomsByName(name: String) { fun chatRoomsByName(name: String) {
val currentServer = serverInteractor.get()!! val currentServer = serverInteractor.get()!!
launchUI(strategy) { launchUI(strategy) {
val roomList = getChatRoomsInteractor.getByName(currentServer, name) try {
view.updateChatRooms(roomList) 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
)
} }
} }
......
...@@ -134,7 +134,9 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -134,7 +134,9 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
} }
private val dismissStatus = { private val dismissStatus = {
connection_status_text.fadeOut() if (connection_status_text != null) {
connection_status_text.fadeOut()
}
} }
private fun setupToolbar() { private fun setupToolbar() {
......
...@@ -7,7 +7,6 @@ import android.content.SharedPreferences ...@@ -7,7 +7,6 @@ import android.content.SharedPreferences
import chat.rocket.android.BuildConfig import chat.rocket.android.BuildConfig
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.app.RocketChatDatabase 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.MemoryTokenRepository
import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository
import chat.rocket.android.dagger.qualifier.ForFresco import chat.rocket.android.dagger.qualifier.ForFresco
...@@ -131,7 +130,6 @@ class AppModule { ...@@ -131,7 +130,6 @@ class AppModule {
listeners.add(RequestLoggingListener()) listeners.add(RequestLoggingListener())
return OkHttpImagePipelineConfigFactory.newBuilder(context, okHttpClient) return OkHttpImagePipelineConfigFactory.newBuilder(context, okHttpClient)
.setImageDecoderConfig(CustomImageFormatConfigurator.createImageDecoderConfig())
.setRequestListeners(listeners) .setRequestListeners(listeners)
.setDownsampleEnabled(true) .setDownsampleEnabled(true)
//.experiment().setBitmapPrepareToDraw(true).experiment() //.experiment().setBitmapPrepareToDraw(true).experiment()
...@@ -141,11 +139,7 @@ class AppModule { ...@@ -141,11 +139,7 @@ class AppModule {
@Provides @Provides
@Singleton @Singleton
fun provideDraweeConfig(): DraweeConfig { fun provideDraweeConfig(): DraweeConfig {
val draweeConfigBuilder = DraweeConfig.newBuilder() return DraweeConfig.newBuilder().build()
CustomImageFormatConfigurator.addCustomDrawableFactories(draweeConfigBuilder)
return draweeConfigBuilder.build()
} }
@Provides @Provides
...@@ -185,7 +179,13 @@ class AppModule { ...@@ -185,7 +179,13 @@ class AppModule {
@Provides @Provides
@Singleton @Singleton
fun provideChatRoomsRepository(): ChatRoomsRepository { fun provideRoomRepository(): RoomRepository {
return MemoryRoomRepository()
}
@Provides
@Singleton
fun provideChatRoomRepository(): ChatRoomsRepository {
return MemoryChatRoomsRepository() return MemoryChatRoomsRepository()
} }
...@@ -207,6 +207,12 @@ class AppModule { ...@@ -207,6 +207,12 @@ class AppModule {
return MemoryMessagesRepository() return MemoryMessagesRepository()
} }
@Provides
@Singleton
fun provideUserRepository(): UsersRepository {
return MemoryUsersRepository()
}
@Provides @Provides
@Singleton @Singleton
fun provideConfiguration(context: Application, client: OkHttpClient): SpannableConfiguration { fun provideConfiguration(context: Application, client: OkHttpClient): SpannableConfiguration {
......
...@@ -11,7 +11,8 @@ object UrlHelper { ...@@ -11,7 +11,8 @@ object UrlHelper {
* @param avatarName The avatar name. * @param avatarName The avatar name.
* @return The avatar URL. * @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. * Returns the CAS URL.
......
...@@ -29,8 +29,14 @@ class MainNavigator(internal val activity: MainActivity, internal val context: C ...@@ -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) { fun toChatRoom(chatRoomId: String,
activity.startActivity(context.chatRoomIntent(chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly, chatRoomLastSeen)) 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) 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 ...@@ -15,6 +15,7 @@ import chat.rocket.android.members.adapter.MembersAdapter
import chat.rocket.android.members.presentation.MembersPresenter import chat.rocket.android.members.presentation.MembersPresenter
import chat.rocket.android.members.presentation.MembersView import chat.rocket.android.members.presentation.MembersView
import chat.rocket.android.members.viewmodel.MemberViewModel 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.inflate
import chat.rocket.android.util.extensions.setVisible import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast import chat.rocket.android.util.extensions.showToast
...@@ -22,6 +23,10 @@ import chat.rocket.android.widget.DividerItemDecoration ...@@ -22,6 +23,10 @@ import chat.rocket.android.widget.DividerItemDecoration
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_members.* import kotlinx.android.synthetic.main.fragment_members.*
import javax.inject.Inject 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 { fun newInstance(chatRoomId: String, chatRoomType: String): Fragment {
return MembersFragment().apply { return MembersFragment().apply {
...@@ -60,6 +65,9 @@ class MembersFragment : Fragment(), MembersView { ...@@ -60,6 +65,9 @@ class MembersFragment : Fragment(), MembersView {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) 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 = "" (activity as AppCompatActivity).supportActionBar?.title = ""
setupRecyclerView() setupRecyclerView()
......
...@@ -25,7 +25,7 @@ class MemberViewModel(private val member: User, private val settings: Map<String ...@@ -25,7 +25,7 @@ class MemberViewModel(private val member: User, private val settings: Map<String
private fun getUserAvatar(): String? { private fun getUserAvatar(): String? {
val username = member.username ?: "?" val username = member.username ?: "?"
return baseUrl?.let { return baseUrl?.let {
UrlHelper.getAvatarUrl(baseUrl, username) UrlHelper.getAvatarUrl(baseUrl, username, "png")
} }
} }
......
...@@ -13,10 +13,10 @@ import chat.rocket.core.internal.rest.setAvatar ...@@ -13,10 +13,10 @@ import chat.rocket.core.internal.rest.setAvatar
import chat.rocket.core.internal.rest.updateProfile import chat.rocket.core.internal.rest.updateProfile
import javax.inject.Inject import javax.inject.Inject
class ProfilePresenter @Inject constructor (private val view: ProfileView, class ProfilePresenter @Inject constructor(private val view: ProfileView,
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
serverInteractor: GetCurrentServerInteractor, serverInteractor: GetCurrentServerInteractor,
factory: RocketChatClientFactory) { factory: RocketChatClientFactory) {
private val serverUrl = serverInteractor.get()!! private val serverUrl = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(serverUrl) private val client: RocketChatClient = factory.create(serverUrl)
private lateinit var myselfId: String private lateinit var myselfId: String
...@@ -29,10 +29,10 @@ class ProfilePresenter @Inject constructor (private val view: ProfileView, ...@@ -29,10 +29,10 @@ class ProfilePresenter @Inject constructor (private val view: ProfileView,
myselfId = myself.id myselfId = myself.id
val avatarUrl = UrlHelper.getAvatarUrl(serverUrl, myself.username!!) val avatarUrl = UrlHelper.getAvatarUrl(serverUrl, myself.username!!)
view.showProfile( view.showProfile(
avatarUrl, avatarUrl,
myself.name!!, myself.name ?: "",
myself.username!!, myself.username ?: "",
myself.emails?.get(0)?.address!! myself.emails?.get(0)?.address!!
) )
} catch (exception: RocketChatException) { } catch (exception: RocketChatException) {
exception.message?.let { exception.message?.let {
......
...@@ -56,8 +56,8 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -56,8 +56,8 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
text_email.textContent = email text_email.textContent = email
text_avatar_url.textContent = "" text_avatar_url.textContent = ""
currentName = name currentName = username
currentUsername = username currentUsername = name
currentEmail = email currentEmail = email
currentAvatar = avatarUrl currentAvatar = avatarUrl
...@@ -129,20 +129,20 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -129,20 +129,20 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
private fun listenToChanges() { private fun listenToChanges() {
Observables.combineLatest(text_name.asObservable(), Observables.combineLatest(text_name.asObservable(),
text_username.asObservable(), text_username.asObservable(),
text_email.asObservable(), text_email.asObservable(),
text_avatar_url.asObservable()) { text_name, text_username, text_email, text_avatar_url -> text_avatar_url.asObservable()) { text_name, text_username, text_email, text_avatar_url ->
return@combineLatest (text_name.toString() != currentName || return@combineLatest (text_name.toString() != currentName ||
text_username.toString() !=currentUsername || text_username.toString() != currentUsername ||
text_email.toString() != currentEmail || text_email.toString() != currentEmail ||
(text_avatar_url.toString() != "" && text_avatar_url.toString()!= currentAvatar)) (text_avatar_url.toString() != "" && text_avatar_url.toString() != currentAvatar))
}.subscribe({ isValid-> }.subscribe({ isValid ->
if (isValid) { if (isValid) {
startActionMode() startActionMode()
} else { } else {
finishActionMode() finishActionMode()
} }
}) })
} }
private fun startActionMode() { private fun startActionMode() {
...@@ -154,7 +154,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -154,7 +154,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
private fun finishActionMode() = actionMode?.finish() private fun finishActionMode() = actionMode?.finish()
private fun enableUserInput(value: Boolean) { private fun enableUserInput(value: Boolean) {
text_name.isEnabled = value text_username.isEnabled = value
text_username.isEnabled = value text_username.isEnabled = value
text_email.isEnabled = value text_email.isEnabled = value
text_avatar_url.isEnabled = value text_avatar_url.isEnabled = value
......
...@@ -6,6 +6,14 @@ import kotlinx.coroutines.experimental.withContext ...@@ -6,6 +6,14 @@ import kotlinx.coroutines.experimental.withContext
import javax.inject.Inject import javax.inject.Inject
class GetChatRoomsInteractor @Inject constructor(private val repository: ChatRoomsRepository) { 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) fun get(url: String) = repository.get(url)
/** /**
......
...@@ -8,6 +8,7 @@ interface MessagesRepository { ...@@ -8,6 +8,7 @@ interface MessagesRepository {
* Get message by its message id. * Get message by its message id.
* *
* @param id The id of the message to get. * @param id The id of the message to get.
*
* @return The Message object given by the id or null if message wasn't found. * @return The Message object given by the id or null if message wasn't found.
*/ */
fun getById(id: String): Message? fun getById(id: String): Message?
...@@ -20,8 +21,19 @@ interface MessagesRepository { ...@@ -20,8 +21,19 @@ interface MessagesRepository {
*/ */
fun getByRoomId(rid: String): List<Message> 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! * Get all messages. Use carefully!
*
* @return All messages or an empty list. * @return All messages or an empty list.
*/ */
fun getAll(): List<Message> 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 { ...@@ -15,6 +15,11 @@ class MemoryMessagesRepository : MessagesRepository {
return messages.filter { it.value.roomId == rid }.values.toList() 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 getAll(): List<Message> = messages.values.toList()
override fun save(message: Message) { 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 chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter
import java.util.concurrent.CopyOnWriteArrayList
internal class StringMatchingCompletionStrategy(private val threshold: Int = -1) : CompletionStrategy {
private val list = CopyOnWriteArrayList<SuggestionModel>()
override fun autocompleteItems(prefix: String): List<SuggestionModel> {
val result = list.filter {
it.searchList.forEach { word ->
if (word.contains(prefix, ignoreCase = true)) {
return@filter true
}
}
false
}.sortedByDescending { it.pinned }
return if (threshold == SuggestionsAdapter.UNLIMITED_RESULT_COUNT) result else result.take(threshold)
}
override fun addAll(list: List<SuggestionModel>) {
this.list.addAllAbsent(list)
}
override fun getItem(prefix: String, position: Int): SuggestionModel {
return list[position]
}
override fun size(): Int {
return list.size
}
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.strategy.trie
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.strategy.CompletionStrategy
import chat.rocket.android.widget.autocompletion.strategy.trie.data.Trie
class TrieCompletionStrategy : CompletionStrategy {
private val items = mutableListOf<SuggestionModel>()
private val trie = Trie()
override fun getItem(prefix: String, position: Int): SuggestionModel {
val item: SuggestionModel
if (prefix.isEmpty()) {
item = items[position]
} else {
item = autocompleteItems(prefix)[position]
}
return item
}
override fun autocompleteItems(prefix: String) = trie.autocompleteItems(prefix)
override fun addAll(list: List<SuggestionModel>) {
items.addAll(list)
list.forEach {
trie.insert(it)
}
}
override fun size() = items.size
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.strategy.trie.data
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
internal class Trie {
private val root = TrieNode(' ')
private var count = 0
fun insert(item: SuggestionModel) {
val sanitizedWord = item.text.trim().toLowerCase()
// Word exists, bail out.
if (search(sanitizedWord)) return
var current = root
sanitizedWord.forEach { ch ->
val child = current.getChild(ch)
if (child == null) {
val node = TrieNode(ch, current)
current.children[ch] = node
current = node
count++
} else {
current = child
}
}
// Set last node as leaf.
if (current != root) {
current.isLeaf = true
current.item = item
}
}
fun search(word: String): Boolean {
val sanitizedWord = word.trim().toLowerCase()
var current = root
sanitizedWord.forEach { ch ->
val child = current.getChild(ch)
if (child == null) {
return false
}
current = child
}
if (current.isLeaf) {
return true
}
return false
}
fun autocomplete(prefix: String): List<String> {
val sanitizedPrefix = prefix.trim().toLowerCase()
var lastNode: TrieNode? = root
sanitizedPrefix.forEach { ch ->
lastNode = lastNode?.getChild(ch)
if (lastNode == null) return emptyList()
}
return lastNode!!.getWords()
}
fun autocompleteItems(prefix: String): List<SuggestionModel> {
val sanitizedPrefix = prefix.trim().toLowerCase()
var lastNode: TrieNode? = root
sanitizedPrefix.forEach { ch ->
lastNode = lastNode?.getChild(ch)
if (lastNode == null) return emptyList()
}
return lastNode!!.getItems()
}
fun getCount() = count
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.strategy.trie.data
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
internal class TrieNode(internal var data: Char,
internal var parent: TrieNode? = null,
internal var isLeaf: Boolean = false,
internal var item: SuggestionModel? = null) {
val children = hashMapOf<Char, TrieNode>()
fun getChild(c: Char): TrieNode? {
children.forEach {
if (it.key == c) return it.value
}
return null
}
fun getWords(): List<String> {
val list = arrayListOf<String>()
if (isLeaf) {
list.add(toString())
}
children.forEach { node ->
node.value.let {
list.addAll(it.getWords())
}
}
return list
}
class X : SuggestionModel("")
fun getItems(): List<SuggestionModel> {
val list = arrayListOf<SuggestionModel>()
if (isLeaf) {
list.add(item!!)
}
children.forEach { node ->
node.value.let {
list.addAll(it.getItems())
}
}
return list
}
override fun toString(): String = if (parent == null) "" else "${parent.toString()}$data"
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.ui
import android.support.v7.widget.RecyclerView
import android.view.View
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
abstract class BaseSuggestionViewHolder(view: View) : RecyclerView.ViewHolder(view) {
abstract fun bind(item: SuggestionModel, itemClickListener: SuggestionsAdapter.ItemClickListener?)
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.ui
import android.content.Context
import android.support.v7.widget.RecyclerView
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.view.WindowManager
import chat.rocket.android.R
internal class PopupRecyclerView : RecyclerView {
private var displayWidth: Int = 0
constructor(context: Context?) : this(context, null)
constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) {
val wm = context!!.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val display = wm.defaultDisplay
val size = DisplayMetrics()
display.getMetrics(size)
val screenWidth = size.widthPixels
displayWidth = screenWidth
}
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
val hSpec = MeasureSpec.makeMeasureSpec(resources.getDimensionPixelSize(
R.dimen.popup_max_height), MeasureSpec.AT_MOST)
val wSpec = MeasureSpec.makeMeasureSpec(displayWidth, MeasureSpec.EXACTLY)
super.onMeasure(wSpec, hSpec)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
super.onLayout(changed, l + 40, t, r - 40, b)
}
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.ui
import android.support.v7.widget.RecyclerView
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.strategy.CompletionStrategy
import chat.rocket.android.widget.autocompletion.strategy.regex.StringMatchingCompletionStrategy
import java.lang.reflect.Type
import kotlin.properties.Delegates
abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>(
val token: String,
val constraint: Int = CONSTRAINT_UNBOUND,
threshold: Int = MAX_RESULT_COUNT) : RecyclerView.Adapter<VH>() {
companion object {
// Any number of results.
const val UNLIMITED_RESULT_COUNT = -1
// Trigger suggestions only if on the line start.
const val CONSTRAINT_BOUND_TO_START = 0
// Trigger suggestions from anywhere.
const val CONSTRAINT_UNBOUND = 1
// Maximum number of results to display by default.
private const val MAX_RESULT_COUNT = 5
}
private var itemType: Type? = null
private var itemClickListener: ItemClickListener? = null
// Called to gather results when no results have previously matched.
private var providerExternal: ((query: String) -> Unit)? = null
// Maximum number of results/suggestions to display.
private var resultsThreshold: Int = if (threshold > 0) threshold else UNLIMITED_RESULT_COUNT
// The strategy used for suggesting completions.
private val strategy: CompletionStrategy = StringMatchingCompletionStrategy(resultsThreshold)
// Current input term to look up for suggestions.
private var currentTerm: String by Delegates.observable("", { _, _, newTerm ->
val items = strategy.autocompleteItems(newTerm)
notifyDataSetChanged()
})
init {
setHasStableIds(true)
}
override fun getItemId(position: Int): Long {
return getItem(position).text.hashCode().toLong()
}
override fun onBindViewHolder(holder: VH, position: Int) {
holder.bind(getItem(position), itemClickListener)
}
override fun getItemCount() = strategy.autocompleteItems(currentTerm).size
private fun getItem(position: Int): SuggestionModel {
return strategy.autocompleteItems(currentTerm)[position]
}
fun autocomplete(newTerm: String) {
this.currentTerm = newTerm.toLowerCase().trim()
}
fun addItems(list: List<SuggestionModel>) {
strategy.addAll(list)
// Since we've just added new items we should check for possible new completion suggestions.
strategy.autocompleteItems(currentTerm)
notifyDataSetChanged()
}
fun setOnClickListener(clickListener: ItemClickListener) {
this.itemClickListener = clickListener
}
fun hasItemClickListener() = itemClickListener != null
/**
* Return the current searched term.
*/
fun term() = this.currentTerm
/**
* Set the maximum number of results to show.
*
* @param threshold The maximum number of suggestions to display.
*/
fun setResultsThreshold(threshold: Int) {
check(threshold > 0)
resultsThreshold = threshold
}
fun cancel() {
strategy.addAll(emptyList())
strategy.autocompleteItems(currentTerm)
notifyDataSetChanged()
}
interface ItemClickListener {
fun onClick(item: SuggestionModel)
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"> <set xmlns:android="http://schemas.android.com/apk/res/android">
<translate <translate
android:duration="800" android:duration="500"
android:fromYDelta="0.0%p" android:fromYDelta="0.0%p"
android:toYDelta="0.0%p" /> android:toYDelta="0.0%p" />
</set> </set>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"> <set xmlns:android="http://schemas.android.com/apk/res/android">
<translate 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:fromYDelta="100.0%"
android:toYDelta="0.0%" /> android:toYDelta="0.0%" />
</set> </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 @@ ...@@ -9,7 +9,6 @@
tools:context=".authentication.login.ui.LoginFragment"> tools:context=".authentication.login.ui.LoginFragment">
<android.support.constraint.ConstraintLayout <android.support.constraint.ConstraintLayout
android:id="@+id/middle_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
......
...@@ -14,19 +14,11 @@ ...@@ -14,19 +14,11 @@
android:layout_centerHorizontal="true" android:layout_centerHorizontal="true"
android:text="@string/title_sign_in_your_server" /> 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 <EditText
android:id="@+id/text_server_url" android:id="@+id/text_server_url"
style="@style/Authentication.EditText" style="@style/Authentication.EditText"
android:layout_below="@id/text_headline" android:layout_below="@id/text_headline"
android:layout_marginStart="0dp" android:layout_marginStart="-4dp"
android:layout_marginTop="32dp" android:layout_marginTop="32dp"
android:layout_toEndOf="@id/text_server_protocol" android:layout_toEndOf="@id/text_server_protocol"
android:cursorVisible="false" android:cursorVisible="false"
...@@ -35,7 +27,15 @@ ...@@ -35,7 +27,15 @@
android:digits="0123456789abcdefghijklmnopqrstuvwxyz.-/:" android:digits="0123456789abcdefghijklmnopqrstuvwxyz.-/:"
android:inputType="textUri" android:inputType="textUri"
android:paddingEnd="0dp" 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 <com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading" android:id="@+id/view_loading"
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/constraint_layout" android:id="@+id/relative_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:focusableInTouchMode="true"
tools:context=".authentication.signup.ui.SignupFragment"> tools:context=".authentication.signup.ui.SignupFragment">
<TextView <ScrollView
android:id="@+id/text_headline" android:id="@+id/scroll_view"
style="@style/Authentication.Headline.TextView" android:layout_width="match_parent"
android:text="@string/title_sign_up" android:layout_height="wrap_content">
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText <LinearLayout
android:id="@+id/text_name" android:layout_width="match_parent"
style="@style/Authentication.EditText" android:layout_height="wrap_content"
android:layout_marginTop="32dp" android:orientation="vertical">
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" />
<EditText <TextView
android:id="@+id/text_username" android:id="@+id/text_headline"
style="@style/Authentication.EditText" style="@style/Authentication.Headline.TextView"
android:layout_marginTop="16dp" android:layout_gravity="center"
android:drawableStart="@drawable/ic_at_black_24dp" android:text="@string/title_sign_up" />
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" />
<EditText <EditText
android:id="@+id/text_password" android:id="@+id/text_name"
style="@style/Authentication.EditText" style="@style/Authentication.EditText"
android:layout_marginTop="16dp" android:layout_marginTop="32dp"
android:drawableStart="@drawable/ic_lock_black_24dp" android:drawableStart="@drawable/ic_person_black_24dp"
android:hint="@string/msg_password" android:hint="@string/msg_name"
android:imeOptions="actionNext" android:imeOptions="actionNext"
android:inputType="textPassword" android:inputType="textCapWords" />
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_username" />
<EditText <EditText
android:id="@+id/text_email" android:id="@+id/text_username"
style="@style/Authentication.EditText" style="@style/Authentication.EditText"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:drawableStart="@drawable/ic_email_black_24dp" android:drawableStart="@drawable/ic_at_black_24dp"
android:hint="@string/msg_email" android:hint="@string/msg_username"
android:imeOptions="actionDone" android:imeOptions="actionNext"
android:inputType="textEmailAddress" android:inputType="text" />
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" <EditText
app:layout_constraintTop_toBottomOf="@+id/text_password" /> android:id="@+id/text_password"
style="@style/Authentication.EditText"
android:layout_marginTop="16dp"
android:drawableStart="@drawable/ic_lock_black_24dp"
android:hint="@string/msg_password"
android:imeOptions="actionNext"
android:inputType="textPassword" />
<EditText
android:id="@+id/text_email"
style="@style/Authentication.EditText"
android:layout_marginBottom="16dp"
android:layout_marginTop="16dp"
android:drawableStart="@drawable/ic_email_black_24dp"
android:hint="@string/msg_email"
android:imeOptions="actionDone"
android:inputType="textEmailAddress" />
</LinearLayout>
</ScrollView>
<com.wang.avi.AVLoadingIndicatorView <com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading" android:id="@+id/view_loading"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="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:layout_marginTop="16dp"
android:visibility="gone" android:visibility="gone"
app:indicatorName="BallPulseIndicator" app:indicatorName="BallPulseIndicator"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_email"
tools:visibility="visible" /> tools:visibility="visible" />
<TextView <LinearLayout
android:id="@+id/text_new_user_agreement" android:id="@+id/bottom_container"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp" android:layout_alignParentBottom="true"
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins" android:orientation="vertical">
android:layout_marginStart="@dimen/screen_edge_left_and_right_margins"
android:gravity="center" <TextView
android:textColorLink="@color/colorAccent" android:id="@+id/text_new_user_agreement"
app:layout_constraintBottom_toTopOf="@+id/button_sign_up" android:layout_width="wrap_content"
app:layout_constraintLeft_toLeftOf="parent" android:layout_height="wrap_content"
app:layout_constraintRight_toRightOf="parent" /> android:layout_gravity="center"
android:layout_margin="@dimen/screen_edge_left_and_right_margins"
android:gravity="center"
android:textColorLink="@color/colorAccent" />
<Button <Button
android:id="@+id/button_sign_up" android:id="@+id/button_sign_up"
style="@style/Authentication.Button" style="@style/Authentication.Button"
android:text="@string/title_sign_up" android:text="@string/title_sign_up" />
app:layout_constraintBottom_toBottomOf="parent" /> </LinearLayout>
</android.support.constraint.ConstraintLayout> </RelativeLayout>
\ No newline at end of file \ No newline at end of file
...@@ -28,8 +28,16 @@ ...@@ -28,8 +28,16 @@
layout="@layout/message_list" layout="@layout/message_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
</FrameLayout> </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 <include
android:id="@+id/layout_message_composer" android:id="@+id/layout_message_composer"
layout="@layout/message_composer" layout="@layout/message_composer"
...@@ -58,15 +66,15 @@ ...@@ -58,15 +66,15 @@
android:id="@+id/connection_status_text" android:id="@+id/connection_status_text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="32dp" android:layout_height="32dp"
android:alpha="0"
android:background="@color/colorPrimary" android:background="@color/colorPrimary"
android:elevation="4dp" android:elevation="4dp"
android:textColor="@color/white"
android:gravity="center" android:gravity="center"
android:textAppearance="@style/TextAppearance.AppCompat.Body2" android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="@color/white"
android:visibility="gone" android:visibility="gone"
android:alpha="0"
tools:alpha="1" tools:alpha="1"
tools:visibility="visible" tools:text="connected"
tools:text="connected"/> tools:visibility="visible" />
</RelativeLayout> </RelativeLayout>
...@@ -27,6 +27,18 @@ ...@@ -27,6 +27,18 @@
android:visibility="gone" android:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/divider" /> 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 <LinearLayout
android:id="@+id/input_container" android:id="@+id/input_container"
android:layout_width="match_parent" android:layout_width="match_parent"
......
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginTop="4dp"
android:background="@color/suggestion_background_color"
android:orientation="horizontal"
android:paddingTop="2dp">
<TextView
android:id="@+id/text_command_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginStart="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/black"
android:textSize="14sp"
tools:text="/leave" />
<TextView
android:id="@+id/text_command_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="16dp"
android:layout_toRightOf="@id/text_command_name"
android:ellipsize="end"
android:gravity="start"
android:maxLines="1"
android:textColor="@color/gray_material"
android:textSize="14sp"
tools:text="Leave a channel" />
</RelativeLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:layout_marginEnd="2dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="2dp"
android:layout_marginStart="4dp"
android:layout_marginTop="2dp"
android:background="@color/suggestion_background_color">
<FrameLayout
android:id="@+id/image_avatar_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_avatar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="4dp"
app:roundedCornerRadius="3dp"
tools:src="@tools:sample/avatars" />
<ImageView
android:id="@+id/image_status"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_gravity="bottom|end"
android:background="@drawable/user_status_white"
android:padding="2dp" />
</FrameLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/image_avatar_container"
android:layout_toRightOf="@id/image_avatar_container"
android:background="@color/suggestion_background_color">
<TextView
android:id="@+id/text_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:maxLines="1"
android:textColor="@color/black"
android:textSize="16sp"
tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/text_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_toEndOf="@+id/text_username"
android:layout_toRightOf="@+id/text_username"
android:maxLines="1"
android:paddingLeft="8dp"
android:paddingStart="8dp"
android:textColor="@color/gray_material"
android:textSize="16sp"
tools:text="@tools:sample/full_names" />
</RelativeLayout>
</RelativeLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="2dp"
android:background="@color/suggestion_background_color">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/image_avatar_container"
android:layout_toRightOf="@id/image_avatar_container"
android:background="@color/suggestion_background_color">
<TextView
android:id="@+id/text_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:maxLines="1"
android:textColor="@color/black"
android:textSize="16sp"
tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/text_fullname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_toEndOf="@+id/text_name"
android:layout_toRightOf="@+id/text_name"
android:maxLines="1"
android:paddingLeft="8dp"
android:paddingStart="8dp"
android:textColor="@color/gray_material"
android:textSize="16sp"
tools:text="@tools:sample/full_names" />
</RelativeLayout>
</RelativeLayout>
\ No newline at end of file
...@@ -24,6 +24,8 @@ ...@@ -24,6 +24,8 @@
<string name="action_logout">Sair</string> <string name="action_logout">Sair</string>
<string name="action_files">Arquivos</string> <string name="action_files">Arquivos</string>
<string name="action_confirm_password">Confirme a nova senha</string> <string name="action_confirm_password">Confirme a nova senha</string>
<string name="action_join_chat">Entrar no Chat</string>
<!-- Settings List --> <!-- Settings List -->
<string-array name="settings_actions"> <string-array name="settings_actions">
...@@ -112,4 +114,28 @@ ...@@ -112,4 +114,28 @@
<string name="status_authenticating">autenticando</string> <string name="status_authenticating">autenticando</string>
<string name="status_disconnecting">desconectando</string> <string name="status_disconnecting">desconectando</string>
<string name="status_waiting">conectando em %d segundos</string> <string name="status_waiting">conectando em %d segundos</string>
<!-- Slash Commands -->
<string name="Slash_Gimme_Description">Exibir ༼ つ ◕_◕ ༽つ antes de sua mensagem</string>
<string name="Slash_LennyFace_Description">Exibir ( ͡° ͜ʖ ͡°) depois de sua mensagem</string>
<string name="Slash_Shrug_Description">Exibir ¯\_(ツ)_/¯ depois de sua mensagem</string>
<string name="Slash_Tableflip_Description">Exibir (╯°□°)╯︵ ┻━┻</string>
<string name="Slash_TableUnflip_Description">Exibir ┬─┬ ノ( ゜-゜ノ)</string>
<string name="Create_A_New_Channel">Criar um novo canal</string>
<string name="Show_the_keyboard_shortcut_list">Show the keyboard shortcut list</string>
<string name="Invite_user_to_join_channel_all_from"> do [#canal] para entrar neste</string>
<string name="Invite_user_to_join_channel_all_to">Convidar todos os usuários deste canal para entrar no [#canal]</string>
<string name="Archive">Arquivar</string>
<string name="Remove_someone_from_room">Remover alguém do canal</string>
<string name="Leave_the_current_channel">Sair do canal atual</string>
<string name="Displays_action_text">Exibir texto de ação</string>
<string name="Direct_message_someone">Enviar DM para alguém</string>
<string name="Mute_someone_in_room">Mutar alguém</string>
<string name="Unmute_someone_in_room">Desmutar alguém na sala</string>
<string name="Invite_user_to_join_channel">Convidar algum usuário para entrar neste canal</string>
<string name="Unarchive">Desarquivar</string>
<string name="Join_the_given_channel">Entrar no canal especificado</string>
<string name="Guggy_Command_Description">Gera um gif baseado no texto dado</string>
<string name="Slash_Topic_Description">Definir tópico</string>
</resources> </resources>
\ No newline at end of file
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
<color name="colorUserStatusOnline">#2FE1A8</color> <color name="colorUserStatusOnline">#2FE1A8</color>
<color name="colorUserStatusBusy">#F33E5B</color> <color name="colorUserStatusBusy">#F33E5B</color>
<color name="colorUserStatusAway">#FDD236</color> <color name="colorUserStatusAway">#FDD236</color>
<color name="colorUserStatusOffline">#1F2228</color> <color name="colorUserStatusOffline">#d9d9d9</color>
<color name="colorDrawableTintGrey">#9FA2A8</color> <color name="colorDrawableTintGrey">#9FA2A8</color>
...@@ -38,4 +38,7 @@ ...@@ -38,4 +38,7 @@
<color name="colorDivider">#1F000000</color> <color name="colorDivider">#1F000000</color>
<!-- Suggestions -->
<color name="suggestion_background_color">@android:color/white</color>
</resources> </resources>
...@@ -32,4 +32,8 @@ ...@@ -32,4 +32,8 @@
<dimen name="padding_mention">4dp</dimen> <dimen name="padding_mention">4dp</dimen>
<dimen name="radius_mention">6dp</dimen> <dimen name="radius_mention">6dp</dimen>
<!-- Autocomplete Popup -->
<dimen name="popup_max_height">150dp</dimen>
<dimen name="suggestions_box_max_height">250dp</dimen>
</resources> </resources>
\ No newline at end of file
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
<string name="action_logout">Logout</string> <string name="action_logout">Logout</string>
<string name="action_files">Files</string> <string name="action_files">Files</string>
<string name="action_confirm_password">Confirm Password Change</string> <string name="action_confirm_password">Confirm Password Change</string>
<string name="action_join_chat">Join Chat</string>
<!-- Settings List --> <!-- Settings List -->
<string-array name="settings_actions"> <string-array name="settings_actions">
...@@ -115,4 +116,27 @@ ...@@ -115,4 +116,27 @@
<string name="status_disconnecting">disconnecting</string> <string name="status_disconnecting">disconnecting</string>
<string name="status_waiting">connecting in %d seconds</string> <string name="status_waiting">connecting in %d seconds</string>
<!-- Slash Commands -->
<string name="Slash_Gimme_Description">Displays ༼ つ ◕_◕ ༽つ before your message</string>
<string name="Slash_LennyFace_Description">Displays ( ͡° ͜ʖ ͡°) after your message</string>
<string name="Slash_Shrug_Description">Displays ¯\_(ツ)_/¯ after your message</string>
<string name="Slash_Tableflip_Description">Displays (╯°□°)╯︵ ┻━┻</string>
<string name="Slash_TableUnflip_Description">Displays ┬─┬ ノ( ゜-゜ノ)</string>
<string name="Create_A_New_Channel">Create a new channel</string>
<string name="Show_the_keyboard_shortcut_list">Show the keyboard shortcut list</string>
<string name="Invite_user_to_join_channel_all_from">Invite all users from [#channel] to join this channel</string>
<string name="Invite_user_to_join_channel_all_to">Invite all users from this channel to join [#channel]</string>
<string name="Archive">Archive</string>
<string name="Remove_someone_from_room">Remove someone from the room</string>
<string name="Leave_the_current_channel">Leave the current channel</string>
<string name="Displays_action_text">Displays action text</string>
<string name="Direct_message_someone">Direct message someone</string>
<string name="Mute_someone_in_room">Mute someone in the room</string>
<string name="Unmute_someone_in_room">Unmute someone in the room</string>
<string name="Invite_user_to_join_channel">Invite one user to join this channel</string>
<string name="Unarchive">Unarchive</string>
<string name="Join_the_given_channel">Join the given channel</string>
<string name="Guggy_Command_Description">Generates a gif based upon the provided text</string>
<string name="Slash_Topic_Description">Set topic</string>
</resources> </resources>
\ No newline at end of file
...@@ -14,7 +14,7 @@ buildscript { ...@@ -14,7 +14,7 @@ buildscript {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}" classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}"
classpath 'com.google.gms:google-services:3.2.0' 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 // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
......
...@@ -4,7 +4,7 @@ ext { ...@@ -4,7 +4,7 @@ ext {
compileSdk : 27, compileSdk : 27,
targetSdk : 27, targetSdk : 27,
buildTools : '27.0.3', buildTools : '27.0.3',
kotlin : '1.2.21', kotlin : '1.2.30',
coroutine : '0.22', coroutine : '0.22',
dokka : '0.9.15', dokka : '0.9.15',
...@@ -25,8 +25,7 @@ ext { ...@@ -25,8 +25,7 @@ ext {
rxBinding : '2.0.0', rxBinding : '2.0.0',
fresco : '1.8.1', fresco : '1.8.1',
kotshi : '0.3.0', kotshi : '0.3.0',
frescoImageViewer : '0.5.0', frescoImageViewer : '0.5.1',
androidSvg : 'master-SNAPSHOT',
markwon : '1.0.3', markwon : '1.0.3',
sheetMenu : '1.3.3', sheetMenu : '1.3.3',
aVLoadingIndicatorView : '2.1.3', aVLoadingIndicatorView : '2.1.3',
...@@ -86,8 +85,7 @@ ext { ...@@ -86,8 +85,7 @@ ext {
kotshiApi : "se.ansman.kotshi:api:${versions.kotshi}", kotshiApi : "se.ansman.kotshi:api:${versions.kotshi}",
kotshiCompiler : "se.ansman.kotshi:compiler:${versions.kotshi}", kotshiCompiler : "se.ansman.kotshi:compiler:${versions.kotshi}",
frescoImageViewer : "com.github.stfalcon:frescoimageviewer:${versions.frescoImageViewer}", frescoImageViewer : "com.github.luciofm:FrescoImageViewer:${versions.frescoImageViewer}",
androidSvg : "com.github.BigBadaboom:androidsvg:${versions.androidSvg}",
markwon : "ru.noties:markwon:${versions.markwon}", markwon : "ru.noties:markwon:${versions.markwon}",
markwonImageLoader : "ru.noties:markwon-image-loader:${versions.markwon}", markwonImageLoader : "ru.noties:markwon-image-loader:${versions.markwon}",
...@@ -96,8 +94,6 @@ ext { ...@@ -96,8 +94,6 @@ ext {
aVLoadingIndicatorView : "com.wang.avi:library:${versions.aVLoadingIndicatorView}", aVLoadingIndicatorView : "com.wang.avi:library:${versions.aVLoadingIndicatorView}",
swipeBackLayout : "me.imid.swipebacklayout.lib:library:${versions.swipeBackLayout}",
// For testing // For testing
junit : "junit:junit:$versions.junit", junit : "junit:junit:$versions.junit",
expressoCore : "com.android.support.test.espresso:espresso-core:${versions.expresso}", 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